diff --git a/README.md b/README.md index c0254d1..845e4be 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,17 @@ C# implementation of FrostFS SDK. +## Prerequisites + +### Get the key for your wallet + +1. Get the address +```bash +cat | jq .accounts[0].address | tr -d '"' +``` + +2. Get the key +```bash +neo-go wallet export -w -d +``` + diff --git a/sdk/FrostFS.SDK.sln b/sdk/FrostFS.SDK.sln new file mode 100644 index 0000000..424a077 --- /dev/null +++ b/sdk/FrostFS.SDK.sln @@ -0,0 +1,40 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ProtosV2", "src\FrostFS.SDK.ProtosV2\FrostFS.SDK.ProtosV2.csproj", "{A087047E-2505-4137-97CC-689A5AD58A0C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ClientV2", "src\FrostFS.SDK.ClientV2\FrostFS.SDK.ClientV2.csproj", "{F9CE6347-111A-4CE6-8BB2-545469838F47}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.Cryptography", "src\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj", "{599E2FF8-12C0-414D-B295-4C971A0A1A63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrostFS.SDK.ModelsV2", "src\FrostFS.SDK.ModelsV2\FrostFS.SDK.ModelsV2.csproj", "{3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A087047E-2505-4137-97CC-689A5AD58A0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A087047E-2505-4137-97CC-689A5AD58A0C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A087047E-2505-4137-97CC-689A5AD58A0C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A087047E-2505-4137-97CC-689A5AD58A0C}.Release|Any CPU.Build.0 = Release|Any CPU + {F9CE6347-111A-4CE6-8BB2-545469838F47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9CE6347-111A-4CE6-8BB2-545469838F47}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9CE6347-111A-4CE6-8BB2-545469838F47}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9CE6347-111A-4CE6-8BB2-545469838F47}.Release|Any CPU.Build.0 = Release|Any CPU + {B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B04EBDF5-A446-49D6-B1D7-7D729D94E8E0}.Release|Any CPU.Build.0 = Release|Any CPU + {599E2FF8-12C0-414D-B295-4C971A0A1A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {599E2FF8-12C0-414D-B295-4C971A0A1A63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {599E2FF8-12C0-414D-B295-4C971A0A1A63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {599E2FF8-12C0-414D-B295-4C971A0A1A63}.Release|Any CPU.Build.0 = Release|Any CPU + {3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F6E5EE6-2AAC-45BE-A5E2-B83D11F90CC3}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/sdk/src/FrostFS.SDK.ClientV2/Client.cs b/sdk/src/FrostFS.SDK.ClientV2/Client.cs new file mode 100644 index 0000000..56aeb4c --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/Client.cs @@ -0,0 +1,94 @@ +using System.Security.Cryptography; +using FrostFS.Container; +using FrostFS.Netmap; +using FrostFS.SDK.Cryptography; +using FrostFS.Session; +using Grpc.Core; +using Grpc.Net.Client; +using Version = FrostFS.SDK.ModelsV2.Version; + +namespace FrostFS.SDK.ClientV2; + +public partial class Client +{ + private readonly ECDsa _key; + private GrpcChannel _channel; + private Version _version = new (2, 13); + + private ContainerService.ContainerServiceClient _containerServiceClient; + private NetmapService.NetmapServiceClient _netmapServiceClient; + private SessionService.SessionServiceClient _sessionServiceClient; + + public Client(string key, string host) + { + // TODO: Развязать клиент и реализацию GRPC + _key = key.LoadWif(); + InitGrpcChannel(host); + InitContainerClient(); + InitNetmapClient(); + InitSessionClient(); + CheckFrostFsVersionSupport(); + } + + private void CheckFrostFsVersionSupport() + { + var localNodeInfo = GetLocalNodeInfoAsync().Result; + var frostFsVersion = new Version( + (int)localNodeInfo.Body.Version.Major, + (int)localNodeInfo.Body.Version.Minor + ); + if (!frostFsVersion.IsSupported(_version)) + { + var msg = $"FrostFS {frostFsVersion} is not supported."; + Console.WriteLine(msg); + throw new ApplicationException(msg); + } + } + + private void InitGrpcChannel(string host) + { + Uri uri; + try + { + uri = new Uri(host); + } + catch (UriFormatException e) + { + var msg = $"Host '{host}' has invalid format. Error: {e.Message}"; + Console.WriteLine(msg); + throw new ArgumentException(msg); + } + + ChannelCredentials grpcCredentials; + switch (uri.Scheme) + { + case "https": + grpcCredentials = ChannelCredentials.SecureSsl; + break; + case "http": + grpcCredentials = ChannelCredentials.Insecure; + break; + default: + var msg = $"Host '{host}' has invalid URI scheme: '{uri.Scheme}'."; + Console.WriteLine(msg); + throw new ArgumentException(msg); + } + + _channel = GrpcChannel.ForAddress(uri, new GrpcChannelOptions { Credentials = grpcCredentials }); + } + + private void InitContainerClient() + { + _containerServiceClient = new ContainerService.ContainerServiceClient(_channel); + } + + private void InitNetmapClient() + { + _netmapServiceClient = new NetmapService.NetmapServiceClient(_channel); + } + + private void InitSessionClient() + { + _sessionServiceClient = new SessionService.SessionServiceClient(_channel); + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj b/sdk/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj new file mode 100644 index 0000000..ec50a7a --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/FrostFS.SDK.ClientV2.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/sdk/src/FrostFS.SDK.ClientV2/RequestConstructor.cs b/sdk/src/FrostFS.SDK.ClientV2/RequestConstructor.cs new file mode 100644 index 0000000..c365b8c --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/RequestConstructor.cs @@ -0,0 +1,26 @@ +using FrostFS.SDK.ModelsV2; +using FrostFS.Session; + +namespace FrostFS.SDK.ClientV2; + +public static class RequestConstructor +{ + public static void AddMetaHeader(this IRequest request, MetaHeader? metaHeader = null) + { + if (request.MetaHeader is not null) return; + + metaHeader ??= MetaHeader.Default(); + + request.MetaHeader = new RequestMetaHeader + { + Version = new Refs.Version + { + Major = (uint)metaHeader.Version.Major, + Minor = (uint)metaHeader.Version.Minor, + + }, + Epoch = (uint)metaHeader.Epoch, + Ttl = (uint)metaHeader.Ttl + }; + } +} diff --git a/sdk/src/FrostFS.SDK.ClientV2/RequestSigner.cs b/sdk/src/FrostFS.SDK.ClientV2/RequestSigner.cs new file mode 100644 index 0000000..ea4f4e2 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/RequestSigner.cs @@ -0,0 +1,97 @@ +using System.Security.Cryptography; +using FrostFS.Refs; +using FrostFS.Session; +using FrostFS.SDK.Cryptography; +using Google.Protobuf; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; + +namespace FrostFS.SDK.ClientV2 +{ + public static class RequestSigner + { + public const int RFC6979SignatureSize = 64; + + 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 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); + var rs = signer.GenerateSignature(hash); + var signature = new byte[RFC6979SignatureSize]; + var rbytes = rs[0].ToByteArrayUnsigned(); + var sbytes = rs[1].ToByteArrayUnsigned(); + var index = RFC6979SignatureSize / 2 - rbytes.Length; + rbytes.CopyTo(signature, index); + index = RFC6979SignatureSize - sbytes.Length; + sbytes.CopyTo(signature, index); + return signature; + } + + public static SignatureRFC6979 SignRFC6979(this ECDsa key, IMessage message) + { + return new SignatureRFC6979 + { + Key = ByteString.CopyFrom(key.PublicKey()), + Sign = ByteString.CopyFrom(key.SignRFC6979(message.ToByteArray())), + }; + } + + public static SignatureRFC6979 SignRFC6979(this ECDsa key, ByteString data) + { + return new SignatureRFC6979 + { + Key = ByteString.CopyFrom(key.PublicKey()), + Sign = ByteString.CopyFrom(key.SignRFC6979(data.ToByteArray())), + }; + } + + public static byte[] SignData(this ECDsa key, byte[] data) + { + var hash = new byte[65]; + hash[0] = 0x04; + key + .SignHash(SHA512.Create().ComputeHash(data)) + .CopyTo(hash, 1); + return hash; + } + + public static Signature SignMessagePart(this ECDsa key, IMessage? data) + { + var data2sign = data is null ? Array.Empty() : data.ToByteArray(); + var sig = new Signature + { + Key = ByteString.CopyFrom(key.PublicKey()), + Sign = ByteString.CopyFrom(key.SignData(data2sign)), + }; + return sig; + } + + public static void Sign(this IVerificableMessage message, ECDsa key) + { + var meta = message.GetMetaHeader(); + IVerificationHeader verify = message switch + { + IRequest => new RequestVerificationHeader(), + IResponse => new ResponseVerificationHeader(), + _ => throw new InvalidOperationException("Unsopported message type") + }; + var verifyOrigin = message.GetVerificationHeader(); + if (verifyOrigin is null) + verify.BodySignature = key.SignMessagePart(message.GetBody()); + verify.MetaSignature = key.SignMessagePart(meta); + verify.OriginSignature = key.SignMessagePart(verifyOrigin); + verify.SetOrigin(verifyOrigin); + message.SetVerificationHeader(verify); + } + } +} diff --git a/sdk/src/FrostFS.SDK.ClientV2/RequestVerifier.cs b/sdk/src/FrostFS.SDK.ClientV2/RequestVerifier.cs new file mode 100644 index 0000000..400c16a --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/RequestVerifier.cs @@ -0,0 +1,84 @@ +using System.Security.Cryptography; +using FrostFS.Refs; +using FrostFS.Session; +using FrostFS.SDK.Cryptography; +using Google.Protobuf; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; + +namespace FrostFS.SDK.ClientV2 { + public static class RequestVerifier + { + public const int RFC6979SignatureSize = 64; + + private static BigInteger[] DecodeSignature(byte[] sig) + { + if (sig.Length != RFC6979SignatureSize) + throw new FormatException($"Wrong signature size, expect={RFC6979SignatureSize}, actual={sig.Length}"); + var rs = new BigInteger[2]; + rs[0] = new BigInteger(1, sig[..32]); + rs[1] = new BigInteger(1, sig[32..]); + return rs; + } + + public static bool VerifyRFC6979(this byte[] public_key, byte[] data, byte[] sig) + { + if (public_key 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 hash = new byte[digest.GetDigestSize()]; + digest.BlockUpdate(data, 0, data.Length); + digest.DoFinal(hash, 0); + signer.Init(false, bc_public_key); + return signer.VerifySignature(hash, rs[0], rs[1]); + } + + public static bool VerifyRFC6979(this SignatureRFC6979 signature, IMessage message) + { + return signature.Key.ToByteArray().VerifyRFC6979(message.ToByteArray(), signature.Sign.ToByteArray()); + } + + public static bool VerifyData(this ECDsa key, byte[] data, byte[] sig) + { + return key.VerifyHash(SHA512.Create().ComputeHash(data), sig[1..]); + } + + public static bool VerifyMessagePart(this Signature sig, IMessage data) + { + 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()); + } + + public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification) + { + if (!verification.MetaSignature.VerifyMessagePart(meta)) return false; + var origin = verification.GetOrigin(); + 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); + } + + public static bool Verify(this IVerificableMessage message) + { + return VerifyMatryoskaLevel(message.GetBody(), message.GetMetaHeader(), message.GetVerificationHeader()); + } + + public static void ProcessResponse(IResponse resp) + { + Console.WriteLine(resp.MetaHeader.Status); + if (!resp.Verify()) + throw new FormatException($"invalid response, type={resp.GetType()}"); + } + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ClientV2/Services/Container.cs b/sdk/src/FrostFS.SDK.ClientV2/Services/Container.cs new file mode 100644 index 0000000..64746b3 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/Services/Container.cs @@ -0,0 +1,130 @@ +using FrostFS.Container; +using FrostFS.Netmap; +using FrostFS.Refs; +using FrostFS.SDK.Cryptography; +using FrostFS.SDK.ModelsV2; +using Google.Protobuf; +using Version = FrostFS.Refs.Version; + +namespace FrostFS.SDK.ClientV2; + +public partial class Client +{ + public async Task GetContainerAsync(ContainerId containerId) + { + var cid = new ContainerID + { + Value = ByteString.CopyFrom(containerId.ToHash()) + }; + var request = new GetRequest + { + Body = new GetRequest.Types.Body + { + ContainerId = cid + }, + }; + request.AddMetaHeader(); + request.Sign(_key); + return await _containerServiceClient.GetAsync(request); + } + + public async Task ListContainersAsync() + { + var request = new ListRequest + { + Body = new ListRequest.Types.Body + { + OwnerId = new OwnerID + { + Value = ByteString.CopyFrom(OwnerId.FromKey(_key).ToHash()) + } + } + }; + request.AddMetaHeader(); + request.Sign(_key); + return await _containerServiceClient.ListAsync(request); + } + + public async Task CreateContainerAsync(ModelsV2.Netmap.PlacementPolicy placementPolicy) + { + var container = new Container.Container + { + Version = new Version + { + Major = 2, + Minor = 13 + }, + OwnerId = new OwnerID + { + Value = ByteString.CopyFrom(OwnerId.FromKey(_key).ToHash()) + }, + PlacementPolicy = new PlacementPolicy + { + Filters = { }, + Selectors = { }, + Replicas = { } + }, + Nonce = ByteString.CopyFrom(Guid.NewGuid().ToBytes()) + }; + foreach (var replica in placementPolicy.Replicas) + { + container.PlacementPolicy.Replicas.Add(new Replica + { + Count = (uint)replica.Count, + Selector = replica.Selector + } + ); + } + var request = new PutRequest + { + Body = new PutRequest.Types.Body + { + Container = container, + Signature = _key.SignRFC6979(container), + } + }; + request.AddMetaHeader(); + request.Sign(_key); + return await _containerServiceClient.PutAsync(request); + } + + public async Task DeleteContainerAsync(ContainerId containerId) + { + var cid = new ContainerID + { + Value = ByteString.CopyFrom(containerId.ToHash()) + }; + var request = new DeleteRequest + { + Body = new DeleteRequest.Types.Body + { + ContainerId = cid, + Signature = _key.SignRFC6979(cid.Value) + } + }; + request.AddMetaHeader(); + 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/sdk/src/FrostFS.SDK.ClientV2/Services/Netmap.cs b/sdk/src/FrostFS.SDK.ClientV2/Services/Netmap.cs new file mode 100644 index 0000000..816eabc --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/Services/Netmap.cs @@ -0,0 +1,28 @@ +using FrostFS.Netmap; + +namespace FrostFS.SDK.ClientV2; + +public partial class Client +{ + public async Task GetLocalNodeInfoAsync() + { + var request = new LocalNodeInfoRequest + { + Body = new LocalNodeInfoRequest.Types.Body { }, + }; + request.AddMetaHeader(); + request.Sign(_key); + return await _netmapServiceClient.LocalNodeInfoAsync(request); + } + + public async Task GetNetworkInfoAsync() + { + var request = new NetworkInfoRequest + { + Body = new NetworkInfoRequest.Types.Body { }, + }; + request.AddMetaHeader(); + request.Sign(_key); + return await _netmapServiceClient.NetworkInfoAsync(request); + } +} diff --git a/sdk/src/FrostFS.SDK.ClientV2/Services/Session.cs b/sdk/src/FrostFS.SDK.ClientV2/Services/Session.cs new file mode 100644 index 0000000..008a270 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ClientV2/Services/Session.cs @@ -0,0 +1,47 @@ +using FrostFS.Refs; +using FrostFS.SDK.ModelsV2; +using FrostFS.Session; +using Google.Protobuf; + +namespace FrostFS.SDK.ClientV2; + +public partial class Client +{ + public async Task CreateSessionAsync(ulong expiration) + { + var request = new CreateRequest + { + Body = new CreateRequest.Types.Body + { + OwnerId = new OwnerID + { + Value = ByteString.CopyFrom(OwnerId.FromKey(_key).ToHash()) + }, + Expiration = expiration, + } + }; + request.AddMetaHeader(); + request.Sign(_key); + return await CreateSession(request); + } + + public async Task CreateSession(CreateRequest request) + { + var resp = await _sessionServiceClient.CreateAsync(request); + return new SessionToken + { + Body = new SessionToken.Types.Body + { + Id = resp.Body.Id, + SessionKey = resp.Body.SessionKey, + OwnerId = request.Body.OwnerId, + Lifetime = new SessionToken.Types.Body.Types.TokenLifetime + { + Exp = request.Body.Expiration, + Iat = resp.MetaHeader.Epoch, + Nbf = resp.MetaHeader.Epoch, + } + } + }; + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.Cryptography/ArrayHelper.cs b/sdk/src/FrostFS.SDK.Cryptography/ArrayHelper.cs new file mode 100644 index 0000000..3a2646c --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/ArrayHelper.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; + +namespace FrostFS.SDK.Cryptography +{ + internal static class ArrayHelper + { + [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) + { + Buffer.BlockCopy(src, 0, dst, p, src.Length); + p += src.Length; + } + + return dst; + } + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.Cryptography/Base58.cs b/sdk/src/FrostFS.SDK.Cryptography/Base58.cs new file mode 100644 index 0000000..50670f5 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Base58.cs @@ -0,0 +1,77 @@ +using System.Numerics; +using System.Text; + +namespace FrostFS.SDK.Cryptography +{ + public static class Base58 + { + public const string Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + + public static byte[] Base58CheckDecode(this string input) + { + if (input is null) throw new ArgumentNullException(nameof(input)); + byte[] buffer = Decode(input); + if (buffer.Length < 4) throw new FormatException(); + byte[] checksum = buffer[0..(buffer.Length - 4)].Sha256().Sha256(); + if (!buffer.AsSpan(^4).SequenceEqual(checksum.AsSpan(..4))) + throw new FormatException(); + var ret = buffer[..^4]; + Array.Clear(buffer, 0, buffer.Length); + return ret; + } + + public static string Base58CheckEncode(this ReadOnlySpan data) + { + byte[] checksum = data.ToArray().Sha256().Sha256(); + Span buffer = stackalloc byte[data.Length + 4]; + data.CopyTo(buffer); + checksum.AsSpan(..4).CopyTo(buffer[data.Length..]); + var ret = Encode(buffer); + buffer.Clear(); + return ret; + } + + public static byte[] Decode(string input) + { + // Decode Base58 string to BigInteger + var bi = BigInteger.Zero; + for (int i = 0; i < input.Length; i++) + { + int digit = Alphabet.IndexOf(input[i]); + if (digit < 0) + throw new FormatException($"Invalid Base58 character '{input[i]}' at position {i}"); + bi = bi * Alphabet.Length + digit; + } + + // Encode BigInteger to byte[] + // Leading zero bytes get encoded as leading `1` characters + int leadingZeroCount = input.TakeWhile(c => c == Alphabet[0]).Count(); + var leadingZeros = new byte[leadingZeroCount]; + if (bi.IsZero) return leadingZeros; + var bytesWithoutLeadingZeros = bi.ToByteArray(isUnsigned: true, isBigEndian: true); + return ArrayHelper.Concat(leadingZeros, bytesWithoutLeadingZeros); + } + + public static string Encode(ReadOnlySpan input) + { + // Decode byte[] to BigInteger + BigInteger value = new(input, isUnsigned: true, isBigEndian: true); + + // Encode BigInteger to Base58 string + var sb = new StringBuilder(); + + while (value > 0) + { + value = BigInteger.DivRem(value, Alphabet.Length, out var remainder); + sb.Insert(0, Alphabet[(int)remainder]); + } + + // Append `1` for each leading 0 byte + for (int i = 0; i < input.Length && input[i] == 0; i++) + { + sb.Insert(0, Alphabet[0]); + } + return sb.ToString(); + } + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj b/sdk/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj new file mode 100644 index 0000000..a6cd5f3 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/FrostFS.SDK.Cryptography.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/sdk/src/FrostFS.SDK.Cryptography/Helper.cs b/sdk/src/FrostFS.SDK.Cryptography/Helper.cs new file mode 100644 index 0000000..1c15458 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Helper.cs @@ -0,0 +1,69 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; +using FrostFS.SDK.Cryptography.Tz; +using Google.Protobuf; +using Org.BouncyCastle.Crypto.Digests; + +namespace FrostFS.SDK.Cryptography; + +public static class Helper +{ + public const int Sha256HashLength = 32; + + internal static byte[] RIPEMD160(this byte[] value) + { + var hash = new byte[20]; + var digest = new RipeMD160Digest(); + digest.BlockUpdate(value, 0, value.Length); + digest.DoFinal(hash, 0); + return hash; + } + + public static byte[] Sha256(this byte[] value) + { + using var sha256 = SHA256.Create(); + return sha256.ComputeHash(value); + } + + internal static byte[] Sha256(this byte[] value, int offset, int count) + { + using var sha256 = SHA256.Create(); + return sha256.ComputeHash(value, offset, count); + } + + internal static byte[] Sha256(this ReadOnlySpan value) + { + var buffer = new byte[32]; + using var sha256 = SHA256.Create(); + sha256.TryComputeHash(value, buffer, out _); + return buffer; + } + + public static ByteString Sha256(this IMessage data) + { + return ByteString.CopyFrom(data.ToByteArray().Sha256()); + } + + public static ByteString Sha256(this ByteString data) + { + return ByteString.CopyFrom(data.ToByteArray().Sha256()); + } + + public static ByteString TzHash(this IMessage data) + { + return ByteString.CopyFrom(new TzHash().ComputeHash(data.ToByteArray())); + } + + public static ByteString TzHash(this ByteString data) + { + return ByteString.CopyFrom(new TzHash().ComputeHash(data.ToByteArray())); + } + + + + public static ulong Murmur64(this byte[] value, uint seed) + { + using var murmur = new Murmur3_128(seed); + return BinaryPrimitives.ReadUInt64LittleEndian(murmur.ComputeHash(value)); + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/Key.cs b/sdk/src/FrostFS.SDK.Cryptography/Key.cs new file mode 100644 index 0000000..c5a3393 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Key.cs @@ -0,0 +1,157 @@ +using System.Buffers.Binary; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using Google.Protobuf; +using Org.BouncyCastle.Asn1.Sec; + +namespace FrostFS.SDK.Cryptography; + +public static class KeyExtension +{ + public const byte NeoAddressVersion = 0x35; + private const int CompressedPublicKeyLength = 33; + private const int UncompressedPublicKeyLength = 65; + + private static readonly uint CheckSigDescriptor = + BinaryPrimitives.ReadUInt32LittleEndian(Encoding.ASCII.GetBytes("System.Crypto.CheckSig").Sha256()); + + public static byte[] Compress(this byte[] publicKey) + { + if (publicKey.Length != UncompressedPublicKeyLength) + throw new FormatException( + $"{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); + return point.GetEncoded(true); + } + + public static byte[] Decompress(this byte[] publicKey) + { + if (publicKey.Length != CompressedPublicKeyLength) + throw new FormatException( + $"{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); + return point.GetEncoded(false); + } + + private static byte[] CreateSignatureRedeemScript(this byte[] publicKey) + { + if (publicKey.Length != CompressedPublicKeyLength) + throw new FormatException( + $"{nameof(CreateSignatureRedeemScript)} argument isn't compressed public key. " + + $"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}" + ); + var script = new byte[] { 0x0c, CompressedPublicKeyLength }; //PUSHDATA1 33 + script = ArrayHelper.Concat(script, publicKey); + script = ArrayHelper.Concat(script, new byte[] { 0x41 }); //SYSCALL + script = ArrayHelper.Concat(script, BitConverter.GetBytes(CheckSigDescriptor)); //Neo_Crypto_CheckSig + return script; + } + + public static byte[] GetScriptHash(this byte[] publicKey) + { + var script = publicKey.CreateSignatureRedeemScript(); + return script.Sha256().RIPEMD160(); + } + + private static string ToAddress(this byte[] scriptHash, byte version) + { + Span data = stackalloc byte[21]; + data[0] = version; + scriptHash.CopyTo(data[1..]); + return Base58.Base58CheckEncode(data); + } + + private static byte[] GetPrivateKeyFromWIF(string wif) + { + if (wif == null) throw new ArgumentNullException(); + var data = wif.Base58CheckDecode(); + if (data.Length != 34 || data[0] != 0x80 || data[33] != 0x01) + throw new FormatException(); + var privateKey = new byte[32]; + Buffer.BlockCopy(data, 1, privateKey, 0, privateKey.Length); + Array.Clear(data, 0, data.Length); + return privateKey; + } + + public static string Address(this ECDsa key) + { + return key.PublicKey().PublicKeyToAddress(); + } + + public static string PublicKeyToAddress(this byte[] publicKey) + { + if (publicKey.Length != CompressedPublicKeyLength) + throw new FormatException( + nameof(publicKey) + + $" isn't encoded compressed public key. " + + $"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}" + ); + return publicKey.GetScriptHash().ToAddress(NeoAddressVersion); + } + + public static byte[] PublicKey(this ECDsa key) + { + var param = key.ExportParameters(false); + var pubkey = new byte[33]; + var pos = 33 - param.Q.X.Length; + + param.Q.X.CopyTo(pubkey, pos); + if (new BigInteger(param.Q.Y.Reverse().Concat(new byte[] { 0x00 }).ToArray()).IsEven) + pubkey[0] = 0x2; + else + pubkey[0] = 0x3; + + return pubkey; + } + + public static byte[] PrivateKey(this ECDsa key) + { + return key.ExportParameters(true).D; + } + + public static ECDsa LoadPrivateKey(this byte[] private_key) + { + var secp256r1 = SecNamedCurves.GetByName("secp256r1"); + var public_key = + secp256r1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, private_key)).GetEncoded(false)[1..]; + var key = ECDsa.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP256, + D = private_key, + Q = new ECPoint + { + X = public_key[..32], + Y = public_key[32..] + } + }); + return key; + } + + public static ECDsa LoadWif(this string wif) + { + var private_key = GetPrivateKeyFromWIF(wif); + return LoadPrivateKey(private_key); + } + + public static ECDsa LoadPublicKey(this byte[] public_key) + { + var public_key_full = public_key.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..] + } + }); + return key; + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.Cryptography/Murmur3_128.cs b/sdk/src/FrostFS.SDK.Cryptography/Murmur3_128.cs new file mode 100644 index 0000000..9975128 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Murmur3_128.cs @@ -0,0 +1,121 @@ +using System.Buffers.Binary; +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography +{ + internal class Murmur3_128 : HashAlgorithm + { + private const ulong c1 = 0x87c37b91114253d5; + private const ulong c2 = 0x4cf5ad432745937f; + private const uint m = 5; + private const uint n1 = 0x52dce729; + private const uint n2 = 0x38495ab5; + + private ulong h1; + private ulong h2; + private readonly uint seed; + private int length; + + public Murmur3_128(uint seed) + { + this.seed = seed; + Initialize(); + } + + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + length += cbSize; + int remainder = cbSize & 15; + int alignedLength = ibStart + (cbSize - remainder); + for (int i = ibStart; i < alignedLength; i += 16) + { + ulong k1 = BinaryPrimitives.ReadUInt64LittleEndian(array.AsSpan(i)); + ulong k2 = BinaryPrimitives.ReadUInt64LittleEndian(array.AsSpan(i + 8)); + + k1 *= c1; + k1 = (k1 << 31) | (k1 >> 33); + k1 *= c2; + h1 ^= k1; + h1 = (h1 << 27) | (h1 >> 37); + h1 += h2; + h1 = h1 * m + n1; + + k2 *= c2; + k2 = (k2 << 33) | (k2 >> 31); + k2 *= c1; + h2 ^= k2; + h2 = (h2 << 31) | (h2 >> 33); + h2 += h1; + h2 = h2 * m + n2; + } + if (remainder > 0) + { + ulong k1 = 0, k2 = 0; + switch (remainder) + { + case 15: k2 ^= (ulong)array[alignedLength + 14] << 48; goto case 14; + case 14: k2 ^= (ulong)array[alignedLength + 13] << 40; goto case 13; + case 13: k2 ^= (ulong)array[alignedLength + 12] << 32; goto case 12; + case 12: k2 ^= (ulong)array[alignedLength + 11] << 24; goto case 11; + case 11: k2 ^= (ulong)array[alignedLength + 10] << 16; goto case 10; + case 10: k2 ^= (ulong)array[alignedLength + 9] << 8; goto case 9; + case 9: + { + k2 ^= (ulong)array[alignedLength + 8]; + k2 *= c2; + k2 = (k2 << 33) | (k2 >> 31); + k2 *= c1; + h2 ^= k2; + goto case 8; + } + case 8: k1 ^= (ulong)array[alignedLength + 7] << 56; goto case 7; + case 7: k1 ^= (ulong)array[alignedLength + 6] << 48; goto case 6; + case 6: k1 ^= (ulong)array[alignedLength + 5] << 40; goto case 5; + case 5: k1 ^= (ulong)array[alignedLength + 4] << 32; goto case 4; + case 4: k1 ^= (ulong)array[alignedLength + 3] << 24; goto case 3; + case 3: k1 ^= (ulong)array[alignedLength + 2] << 16; goto case 2; + case 2: k1 ^= (ulong)array[alignedLength + 1] << 8; goto case 1; + case 1: + { + k1 ^= (ulong)array[alignedLength]; + k1 *= c1; + k1 = (k1 << 31) | (k1 >> 33); + k1 *= c2; + h1 ^= k1; + break; + } + } + } + } + + protected override byte[] HashFinal() + { + h1 ^= (ulong)length; + h2 ^= (ulong)length; + h1 += h2; + h2 += h1; + h1 = Fimix64(h1); + h2 = Fimix64(h2); + h1 += h2; + h2 += h1; + return BitConverter.GetBytes(h1); + } + + public override void Initialize() + { + length = 0; + h1 = seed; + h2 = seed; + } + + private ulong Fimix64(ulong k) + { + k ^= k >> 33; + k *= 0xff51afd7ed558ccd; + k ^= k >> 33; + k *= 0xc4ceb9fe1a85ec53; + k ^= k >> 33; + return k; + } + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/Tz/GF127.cs b/sdk/src/FrostFS.SDK.Cryptography/Tz/GF127.cs new file mode 100644 index 0000000..bc67b90 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Tz/GF127.cs @@ -0,0 +1,254 @@ +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography.Tz +{ + // GF127 represents element of GF(2^127) + public class GF127 : IEquatable + { + public const int ByteSize = 16; + public const ulong MaxUlong = ulong.MaxValue; + public const ulong MSB64 = (ulong)1 << 63; // 2^63 + public static readonly GF127 Zero = new(0, 0); + public static readonly GF127 One = new(1, 0); + public static readonly GF127 X127X631 = new(MSB64 + 1, MSB64); // x^127+x^63+1 + + private readonly ulong[] _data; + + public ulong this[int index] + { + get { return _data[index]; } + set { _data[index] = value; } + } + + public GF127(ulong[] value) + { + if (value is null || value.Length != 2) + throw new ArgumentException(nameof(value) + "is invalid"); + _data = value; + } + + // Constructs new element of GF(2^127) as u1*x^64 + u0. + // It is assumed that u1 has zero MSB. + public GF127(ulong u0, ulong u1) : this(new ulong[] { u0, u1 }) + { + } + + public GF127() : this(0, 0) + { + } + + public override bool Equals(object obj) + { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj is GF127 b) + return Equals(b); + return false; + } + + public override int GetHashCode() + { + return this[0].GetHashCode() + this[1].GetHashCode(); + } + + public bool Equals(GF127 other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return this[0] == other[0] && this[1] == other[1]; + } + + // return the index of MSB + private int IndexOfMSB() + { + int i = Helper.GetLeadingZeros(this[1]); + if (i == 64) + i += Helper.GetLeadingZeros(this[0]); + return 127 - i; + } + + // Set index n to 1 + public static GF127 SetN(int n) + { + if (n < 64) + return new GF127((ulong)1 << n, 0); + return new GF127(0, (ulong)1 << (n - 64)); + } + + // Add + public static GF127 operator +(GF127 a, GF127 b) + { + return new GF127(a[0] ^ b[0], a[1] ^ b[1]); + } + + // Bitwise-and + public static GF127 operator &(GF127 a, GF127 b) + { + return new GF127(a[0] & b[0], a[1] & b[1]); + } + + // Multiply + public static GF127 operator *(GF127 a, GF127 b) // 2^63 * 2, 10 + { + GF127 r = new(); + GF127 c = a; + + if (b[1] == 0) + { + for (int i = 0; i < b[0].GetNonZeroLength(); i++) + { + if ((b[0] & ((ulong)1 << i)) != 0) + r += c; + c = Mul10(c); // c = c * 2 + } + } + else + { + for (int i = 0; i < 64; i++) + { + if ((b[0] & ((ulong)1 << i)) != 0) + r += c; + c = Mul10(c); // c = c * 2 + } + + for (int i = 0; i < b[1].GetNonZeroLength(); i++) + { + if ((b[1] & ((ulong)1 << i)) != 0) + r += c; + c = Mul10(c); + } + } + + return r; + } + + // Inverse, returns a^-1 + // Extended Euclidean Algorithm + // https://link.springer.com/content/pdf/10.1007/3-540-44499-8_1.pdf + public static GF127 Inv(GF127 a) + { + GF127 v = X127X631, + u = a, + c = new(1, 0), + d = new(0, 0), + t, + x; + + int du = u.IndexOfMSB(); + int dv = v.IndexOfMSB(); + // degree of polynomial is a position of most significant bit + while (du != 0) + { + if (du < dv) + { + (v, u) = (u, v); + (dv, du) = (du, dv); + (d, c) = (c, d); + } + + x = SetN(du - dv); + t = x * v; + u += t; + // because * performs reduction on t, manually reduce u at first step + if (u.IndexOfMSB() == 127) + u += X127X631; + + t = x * d; + c += t; + + du = u.IndexOfMSB(); + dv = v.IndexOfMSB(); + } + + return c; + } + + // Mul10 returns a*x + public static GF127 Mul10(GF127 a) + { + GF127 b = new(); + var c = (a[0] & MSB64) >> 63; + b[0] = a[0] << 1; + b[1] = (a[1] << 1) ^ c; + if ((b[1] & MSB64) != 0) + { + b[0] ^= X127X631[0]; + b[1] ^= X127X631[1]; + } + + return b; + } + + // Mul11 returns a*(x+1) + public static GF127 Mul11(GF127 a) + { + GF127 b = new(); + var c = (a[0] & MSB64) >> 63; + b[0] = a[0] ^ (a[0] << 1); + b[1] = a[1] ^ (a[1] << 1) ^ c; + if ((b[1] & MSB64) == 0) return b; + b[0] ^= X127X631[0]; + b[1] ^= X127X631[1]; + return b; + } + + // Random returns random element from GF(2^127). + // Is used mostly for testing. + public static GF127 Random() + { + using RandomNumberGenerator rng = RandomNumberGenerator.Create(); + return new GF127(rng.NextUlong(), rng.NextUlong() >> 1); + } + + // FromByteArray does the deserialization stuff + public GF127 FromByteArray(byte[] data) + { + if (data.Length != ByteSize) + throw new ArgumentException( + nameof(data) + $" wrong data lenght, {nameof(GF127)} expect={ByteSize}, actual={data.Length}" + ); + var t0 = new byte[8]; + var t1 = new byte[8]; + Array.Copy(data, 0, t1, 0, 8); + Array.Copy(data, 8, t0, 0, 8); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(t0); + Array.Reverse(t1); + } + + _data[0] = BitConverter.ToUInt64(t0); + _data[1] = BitConverter.ToUInt64(t1); + if ((_data[1] & MSB64) != 0) + throw new ArgumentException(nameof(data) + " invalid data"); + return this; + } + + // ToArray() represents element of GF(2^127) as byte array of length 16. + public byte[] ToByteArray() + { + var buff = new byte[16]; + var b0 = BitConverter.GetBytes(_data[0]); + var b1 = BitConverter.GetBytes(_data[1]); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(b0); + Array.Reverse(b1); + } + + Array.Copy(b1, 0, buff, 0, 8); + Array.Copy(b0, 0, buff, 8, 8); + return buff; + } + + // ToString() returns hex-encoded representation, starting with MSB. + public override string ToString() + { + return Convert.ToHexString(ToByteArray()); + } + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.Cryptography/Tz/Helper.cs b/sdk/src/FrostFS.SDK.Cryptography/Tz/Helper.cs new file mode 100644 index 0000000..4aa11ce --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Tz/Helper.cs @@ -0,0 +1,30 @@ +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography.Tz +{ + public static class Helper + { + public static ulong NextUlong(this RandomNumberGenerator rng) + { + var buff = new byte[8]; + rng.GetBytes(buff); + return BitConverter.ToUInt64(buff, 0); + } + + public static int GetLeadingZeros(ulong value) + { + var i = 64; + while (value != 0) + { + value >>= 1; + i--; + } + return i; + } + + public static int GetNonZeroLength(this ulong value) + { + return 64 - GetLeadingZeros(value); + } + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/Tz/SL2.cs b/sdk/src/FrostFS.SDK.Cryptography/Tz/SL2.cs new file mode 100644 index 0000000..9949681 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Tz/SL2.cs @@ -0,0 +1,177 @@ +namespace FrostFS.SDK.Cryptography.Tz +{ + public class SL2 : IEquatable + { + // 2x2 matrix + private readonly GF127[][] data; + + public static readonly SL2 ID = new(new GF127(1, 0), new GF127(0, 0), + new GF127(0, 0), new GF127(1, 0)); + public static readonly SL2 A = new(new GF127(2, 0), new GF127(1, 0), + new GF127(1, 0), new GF127(0, 0)); + public static readonly SL2 B = new(new GF127(2, 0), new GF127(3, 0), + new GF127(1, 0), new GF127(1, 0)); + + // Indexer + public GF127[] this[int i] + { + get { return data[i]; } + set { data[i] = value; } + } + + public SL2(GF127[][] value) + { + if (value is null || value.Length != 2 || !value.All(p => p.Length == 2)) + throw new ArgumentException(nameof(value) + $" invalid {nameof(GF127)} matrics"); + data = value; + } + + public SL2(GF127 g00, GF127 g01, GF127 g10, GF127 g11) + : this(new GF127[][] { new[] { g00, g01 }, new[] { g10, g11 } }) + { + } + + public SL2() : this(GF127.One, GF127.Zero, GF127.Zero, GF127.One) + { + } + + public override bool Equals(object obj) + { + if (obj is null) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj is SL2 b) + return Equals(b); + return false; + } + + public override int GetHashCode() + { + return this[0][0].GetHashCode() + + this[0][1].GetHashCode() + + this[1][0].GetHashCode() + + this[1][1].GetHashCode(); + } + + public bool Equals(SL2 other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return this[0][0].Equals(other[0][0]) && + this[0][1].Equals(other[0][1]) && + this[1][0].Equals(other[1][0]) && + this[1][1].Equals(other[1][1]); + } + + + // 2X2 matrix multiplication + public static SL2 operator *(SL2 a, SL2 b) + { + return new SL2(a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1], + a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]); + } + + // Multiplication using strassen algorithm + public static SL2 MulStrassen(SL2 a, SL2 b) + { + GF127[] t = new GF127[7]; + t[0] = (a[0][0] + a[1][1]) * (b[0][0] + b[1][1]); // t[0] == (a11 + a22) * (b11 + b22) + + t[1] = (a[1][0] + a[1][1]) * b[0][0]; // t[1] == (a21 + a22) * b11 + + t[2] = (b[0][1] + b[1][1]) * a[0][0]; // t[2] == (b12 + b22) * a11 + + t[3] = (b[1][0] + b[0][0]) * a[1][1]; // t[3] == (b21 + b11) * a22 + + t[4] = (a[0][0] + a[0][1]) * b[1][1]; // t[4] == (a11 + a12) * b22 + + t[5] = (a[1][0] + a[0][0]) * (b[0][0] + b[0][1]); // t[5] == (a21 + a11) * (b11 + b12) + + t[6] = (a[0][1] + a[1][1]) * (b[1][0] + b[1][1]); // t[6] == (a12 + a22) * (b21 + b22) + + SL2 r = new(); + r[0][1] = t[2] + t[4]; // r12 == a11*b12 + a11*b22 + a11*b22 + a12*b22 == a11*b12 + a12*b22 + r[1][0] = t[1] + t[3]; // r21 == a21*b11 + a22*b11 + a22*b21 + a22*b11 == a21*b11 + a22*b21 + // r11 == (a11*b11 + a22*b11` + a11*b22` + a22*b22`) + (a22*b21` + a22*b11`) + (a11*b22` + a12*b22`) + + // (a12*b21 + a22*b21` + a12*b22` + a22*b22`) == a11*b11 + a12*b21 + r[0][0] = t[0] + t[3] + t[4] + t[6]; + // r22 == (a11*b11` + a22*b11` + a11*b22` + a22*b22) + (a21*b11` + a22*b11`) + (a11*b12` + a11*b22`) + + // (a21*b11` + a11*b11` + a21*b12 + a11*b12`) == a21*b12 + a22*b22 + r[1][1] = t[0] + t[1] + t[2] + t[5]; + + return r; + } + + // Inv() returns inverse of a in SL2(GF(2^127)) + public static SL2 Inv(SL2 a) + { + GF127[] t = new GF127[2]; + t[0] = a[0][0] * a[1][1] + a[0][1] * a[1][0]; // + t[1] = GF127.Inv(t[0]); + + SL2 r = new(); + r[1][1] = t[1] * a[0][0]; + r[0][1] = t[1] * a[0][1]; + r[1][0] = t[1] * a[1][0]; + r[0][0] = t[1] * a[1][1]; + + return r; + } + + // MulA() returns this*A, A = {{x, 1}, {1, 0}} + public SL2 MulA() + { + var r = new SL2(); + r[0][0] = GF127.Mul10(this[0][0]) + this[0][1]; // r11 == t11*x + t12 + r[0][1] = this[0][0]; // r12 == t11 + + r[1][0] = GF127.Mul10(this[1][0]) + this[1][1]; // r21 == t21*x + t22 + r[1][1] = this[1][0]; // r22 == t21 + + return r; + } + + // MulB() returns this*B, B = {{x, x+1}, {1, 1}} + public SL2 MulB() + { + var r = new SL2(); + r[0][0] = GF127.Mul10(this[0][0]) + this[0][1]; // r11 == t11*x + t12 + r[0][1] = GF127.Mul10(this[0][0]) + this[0][0] + this[0][1]; // r12 == t11*x + t11 + t12 + + r[1][0] = GF127.Mul10(this[1][0]) + this[1][1]; // r21 == t21*x + t22 + r[1][1] = GF127.Mul10(this[1][0]) + this[1][0] + this[1][1]; // r22 == t21*x + t21 + t22 + + return r; + } + + public SL2 FromByteArray(byte[] data) + { + if (data.Length != 64) + throw new ArgumentException(nameof(SL2) + $" invalid data, exect={64}, ecatual={data.Length}"); + this[0][0] = new GF127().FromByteArray(data[0..16]); + this[0][1] = new GF127().FromByteArray(data[16..32]); + this[1][0] = new GF127().FromByteArray(data[32..48]); + this[1][1] = new GF127().FromByteArray(data[48..64]); + return this; + } + + public byte[] ToByteArray() + { + var buff = new byte[64]; + Array.Copy(this[0][0].ToByteArray(), 0, buff, 0, 16); + Array.Copy(this[0][1].ToByteArray(), 0, buff, 16, 16); + Array.Copy(this[1][0].ToByteArray(), 0, buff, 32, 16); + Array.Copy(this[1][1].ToByteArray(), 0, buff, 48, 16); + return buff; + } + + public override string ToString() + { + return this[0][0].ToString() + this[0][1].ToString() + + this[1][0].ToString() + this[1][1].ToString(); + } + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs b/sdk/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs new file mode 100644 index 0000000..e770631 --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/Tz/TzHash.cs @@ -0,0 +1,139 @@ +using System.Security; +using System.Security.Cryptography; + +namespace FrostFS.SDK.Cryptography.Tz +{ + public class TzHash : HashAlgorithm + { + private const int TzHashLength = 64; + private GF127[] x; + public override int HashSize => TzHashLength; + + public TzHash() + { + Initialize(); + } + + public override void Initialize() + { + x = new GF127[4]; + Reset(); + HashValue = null; + } + + public void Reset() + { + x[0] = new GF127(1, 0); + x[1] = new GF127(0, 0); + x[2] = new GF127(0, 0); + x[3] = new GF127(1, 0); + } + + public byte[] ToByteArray() + { + var buff = new byte[HashSize]; + for (int i = 0; i < 4; i++) + { + Array.Copy(x[i].ToByteArray(), 0, buff, i * 16, 16); + } + return buff; + } + + [SecurityCritical] + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + _ = HashData(array[ibStart..(ibStart + cbSize)]); + } + + [SecurityCritical] + protected override byte[] HashFinal() + { + return HashValue = ToByteArray(); + } + + [SecurityCritical] + private int HashData(byte[] data) + { + var n = data.Length; + for (int i = 0; i < n; i++) + { + for (int j = 7; j >= 0; j--) + { + MulBitRight(ref x[0], ref x[1], ref x[2], ref x[3], (data[i] & (1 << j)) != 0); + } + } + return n; + } + + // MulBitRight() multiply A (if the bit is 0) or B (if the bit is 1) on the right side + private void MulBitRight(ref GF127 c00, ref GF127 c01, ref GF127 c10, ref GF127 c11, bool bit) + { + // plan 1 + GF127 t; + if (bit) + { // MulB + t = c00; + c00 = GF127.Mul10(c00) + c01; // c00 = c00 * x + c01 + c01 = GF127.Mul11(t) + c01; // c01 = c00 * (x+1) + c01 + + t = c10; + c10 = GF127.Mul10(c10) + c11; // c10 = c10 * x + c11 + c11 = GF127.Mul11(t) + c11; // c11 = c10 * (x+1) + c11 + } + else + { // MulA + t = c00; + c00 = GF127.Mul10(c00) + c01; // c00 = c00 * x + c01 + c01 = t; // c01 = c00 + + t = c10; + c10 = GF127.Mul10(c10) + c11; // c10 = c10 * x + c11 + c11 = t; // c11 = c10; + } + + //// plan 2 + //var r = new SL2(c00, c01, c10, c11); + //if (bit) + // r.MulB(); + //else + // r.MulA(); + } + + // Concat() performs combining of hashes based on homomorphic characteristic. + public static byte[] Concat(List hs) + { + var r = SL2.ID; + foreach (var h in hs) + { + r *= new SL2().FromByteArray(h); + } + return r.ToByteArray(); + } + + // Validate() checks if hashes in hs combined are equal to h. + public static bool Validate(byte[] h, List hs) + { + var expected = new SL2().FromByteArray(h); + var actual = new SL2().FromByteArray(Concat(hs)); + return expected.Equals(actual); + } + + // SubtractR() returns hash a, such that Concat(a, b) == c + public static byte[] SubstractR(byte[] b, byte[] c) + { + var t1 = new SL2().FromByteArray(b); + var t2 = new SL2().FromByteArray(c); + var r = t2 * SL2.Inv(t1); + return r.ToByteArray(); + } + + // SubtractL() returns hash b, such that Concat(a, b) == c + public static byte[] SubstractL(byte[] a, byte[] c) + { + var t1 = new SL2().FromByteArray(a); + var t2 = new SL2().FromByteArray(c); + var r = SL2.Inv(t1) * t2; + return r.ToByteArray(); + } + } +} diff --git a/sdk/src/FrostFS.SDK.Cryptography/UUID.cs b/sdk/src/FrostFS.SDK.Cryptography/UUID.cs new file mode 100644 index 0000000..3631c5e --- /dev/null +++ b/sdk/src/FrostFS.SDK.Cryptography/UUID.cs @@ -0,0 +1,17 @@ +using Google.Protobuf; + +namespace FrostFS.SDK.Cryptography +{ + public static class UUIDExtension + { + public static Guid ToUuid(this ByteString id) + { + return Guid.Parse(Convert.ToHexString(id.ToByteArray())); + } + + public static byte[] ToBytes(this Guid id) + { + return Convert.FromHexString(id.ToString("N")); + } + } +} diff --git a/sdk/src/FrostFS.SDK.ModelsV2/ContainerId.cs b/sdk/src/FrostFS.SDK.ModelsV2/ContainerId.cs new file mode 100644 index 0000000..65695b9 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/ContainerId.cs @@ -0,0 +1,32 @@ +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.ModelsV2; + +public class ContainerId +{ + public string Value { get; } + + public ContainerId(string id) + { + Value = id; + } + + public static ContainerId FromHash(byte[] hash) + { + if (hash.Length != Helper.Sha256HashLength) + { + throw new FormatException("ContainerID must be a sha256 hash."); + } + return new ContainerId(Base58.Encode(hash)); + } + + public byte[] ToHash() + { + return Base58.Decode(Value); + } + + public override string ToString() + { + return Value; + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj b/sdk/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj new file mode 100644 index 0000000..ba8c5fd --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/sdk/src/FrostFS.SDK.ModelsV2/MetaHeader.cs b/sdk/src/FrostFS.SDK.ModelsV2/MetaHeader.cs new file mode 100644 index 0000000..68abbb3 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/MetaHeader.cs @@ -0,0 +1,27 @@ +namespace FrostFS.SDK.ModelsV2; + +public class MetaHeader +{ + public Version Version { get; set; } + public int Epoch { get; set; } + public int Ttl { get; set; } + + public MetaHeader(Version version, int epoch, int ttl) + { + Version = version; + Epoch = epoch; + Ttl = ttl; + } + + public static MetaHeader Default() + { + return new MetaHeader( + new Version( + major: 2, + minor: 13 + ), + epoch: 0, + ttl: 2 + ); + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs b/sdk/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs new file mode 100644 index 0000000..fc95b1c --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/Netmap/PlacementPolicy.cs @@ -0,0 +1,7 @@ +namespace FrostFS.SDK.ModelsV2.Netmap; + +public class PlacementPolicy +{ + public List Replicas { get; set; } + public bool Unique { get; set; } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs b/sdk/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs new file mode 100644 index 0000000..d58a72b --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/Netmap/Replica.cs @@ -0,0 +1,7 @@ +namespace FrostFS.SDK.ModelsV2.Netmap; + +public class Replica +{ + public int Count { get; set; } + public string Selector { get; set; } = String.Empty; +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ModelsV2/OwnerId.cs b/sdk/src/FrostFS.SDK.ModelsV2/OwnerId.cs new file mode 100644 index 0000000..d223be5 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/OwnerId.cs @@ -0,0 +1,24 @@ +using System.Security.Cryptography; +using FrostFS.SDK.Cryptography; + +namespace FrostFS.SDK.ModelsV2; + +public class OwnerId +{ + public string Value { get; } + + public OwnerId(string id) + { + Value = id; + } + + public static OwnerId FromKey(ECDsa key) + { + return new OwnerId(key.PublicKey().PublicKeyToAddress()); + } + + public byte[] ToHash() + { + return Base58.Decode(Value); + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ModelsV2/Version.cs b/sdk/src/FrostFS.SDK.ModelsV2/Version.cs new file mode 100644 index 0000000..3d852fb --- /dev/null +++ b/sdk/src/FrostFS.SDK.ModelsV2/Version.cs @@ -0,0 +1,23 @@ +namespace FrostFS.SDK.ModelsV2; + +public class Version +{ + public int Major { get; set; } + public int Minor { get; set; } + + public Version(int major, int minor) + { + Major = major; + Minor = minor; + } + + public bool IsSupported(Version version) + { + return Major == version.Major; + } + + public override string ToString() + { + return $"v{Major}.{Minor}"; + } +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj b/sdk/src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj new file mode 100644 index 0000000..2100252 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/FrostFS.SDK.ProtosV2.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IMetaHeader.cs b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IMetaHeader.cs new file mode 100644 index 0000000..5db1515 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IMetaHeader.cs @@ -0,0 +1,9 @@ +using Google.Protobuf; + +namespace FrostFS.Session +{ + public interface IMetaHeader : IMessage + { + IMetaHeader GetOrigin(); + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs new file mode 100644 index 0000000..6dd9cf7 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IRequest.cs @@ -0,0 +1,10 @@ +using FrostFS.Session; + +namespace FrostFS.Session +{ + public interface IRequest : IVerificableMessage + { + RequestMetaHeader MetaHeader { get; set; } + RequestVerificationHeader VerifyHeader { get; set; } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs new file mode 100644 index 0000000..f9fa1bb --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IResponse.cs @@ -0,0 +1,8 @@ +namespace FrostFS.Session +{ + public interface IResponse : IVerificableMessage + { + ResponseMetaHeader MetaHeader { get; set; } + ResponseVerificationHeader VerifyHeader { get; set; } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs new file mode 100644 index 0000000..3141874 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerifiableMessage.cs @@ -0,0 +1,13 @@ +using Google.Protobuf; + +namespace FrostFS.Session +{ + public interface IVerificableMessage : IMessage + { + IMetaHeader GetMetaHeader(); + void SetMetaHeader(IMetaHeader metaHeader); + IVerificationHeader GetVerificationHeader(); + void SetVerificationHeader(IVerificationHeader verificationHeader); + IMessage GetBody(); + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs new file mode 100644 index 0000000..19da0e2 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/Interfaces/IVerificationHeader.cs @@ -0,0 +1,14 @@ +using FrostFS.Refs; +using Google.Protobuf; + +namespace FrostFS.Session +{ + public interface IVerificationHeader : IMessage + { + Signature BodySignature { get; set; } + Signature MetaSignature { get; set; } + Signature OriginSignature { get; set; } + IVerificationHeader GetOrigin(); + void SetOrigin(IVerificationHeader verificationHeader); + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/accounting/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/accounting/service.proto new file mode 100644 index 0000000..715ef63 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/accounting/service.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +package neo.fs.v2.accounting; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting/grpc;accounting"; +option csharp_namespace = "FrostFS.Accounting"; + +import "accounting/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// Accounting service provides methods for interaction with NeoFS sidechain via +// other NeoFS nodes to get information about the account balance. Deposit and +// Withdraw operations can't be implemented here, as they require Mainnet NeoFS +// smart contract invocation. Transfer operations between internal NeoFS +// accounts are possible if both use the same token type. +service AccountingService { + // Returns the amount of funds in GAS token for the requested NeoFS account. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // balance has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc Balance(BalanceRequest) returns (BalanceResponse); +} + +// BalanceRequest message +message BalanceRequest { + // To indicate the account for which the balance is requested, its identifier + // is used. It can be any existing account in NeoFS sidechain `Balance` smart + // contract. If omitted, client implementation MUST set it to the request's + // signer `OwnerID`. + message Body { + // Valid user identifier in `OwnerID` format for which the balance is + // requested. Required field. + neo.fs.v2.refs.OwnerID owner_id = 1; + } + // Body of the balance request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// BalanceResponse message +message BalanceResponse { + // The amount of funds in GAS token for the `OwnerID`'s account requested. + // Balance is given in the `Decimal` format to avoid precision issues with + // rounding. + message Body { + // Amount of funds in GAS token for the requested account. + Decimal balance = 1; + } + // Body of the balance response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/accounting/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/accounting/types.proto new file mode 100644 index 0000000..825412e --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/accounting/types.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package neo.fs.v2.accounting; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/accounting/grpc;accounting"; +option csharp_namespace = "FrostFS.Accounting"; + +// Standard floating point data type can't be used in NeoFS due to inexactness +// of the result when doing lots of small number operations. To solve the lost +// precision issue, special `Decimal` format is used for monetary computations. +// +// Please see [The General Decimal Arithmetic +// Specification](http://speleotrove.com/decimal/) for detailed problem +// description. +message Decimal { + // Number in the smallest Token fractions. + int64 value = 1 [ json_name = "value" ]; + + // Precision value indicating how many smallest fractions can be in one + // integer. + uint32 precision = 2 [ json_name = "precision" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/acl/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/acl/types.proto new file mode 100644 index 0000000..186f08f --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/acl/types.proto @@ -0,0 +1,227 @@ +syntax = "proto3"; + +package neo.fs.v2.acl; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/acl/grpc;acl"; +option csharp_namespace = "FrostFS.Acl"; + +import "refs/types.proto"; + +// Target role of the access control rule in access control list. +enum Role { + // Unspecified role, default value + ROLE_UNSPECIFIED = 0; + + // User target rule is applied if sender is the owner of the container + USER = 1; + + // System target rule is applied if sender is a storage node within the + // container or an inner ring node + SYSTEM = 2; + + // Others target rule is applied if sender is neither a user nor a system + // target + OTHERS = 3; +} + +// MatchType is an enumeration of match types. +enum MatchType { + // Unspecified match type, default value. + MATCH_TYPE_UNSPECIFIED = 0; + + // Return true if strings are equal + STRING_EQUAL = 1; + + // Return true if strings are different + STRING_NOT_EQUAL = 2; +} + +// Request's operation type to match if the rule is applicable to a particular +// request. +enum Operation { + // Unspecified operation, default value + OPERATION_UNSPECIFIED = 0; + + // Get + GET = 1; + + // Head + HEAD = 2; + + // Put + PUT = 3; + + // Delete + DELETE = 4; + + // Search + SEARCH = 5; + + // GetRange + GETRANGE = 6; + + // GetRangeHash + GETRANGEHASH = 7; +} + +// Rule execution result action. Either allows or denies access if the rule's +// filters match. +enum Action { + // Unspecified action, default value + ACTION_UNSPECIFIED = 0; + + // Allow action + ALLOW = 1; + + // Deny action + DENY = 2; +} + +// Enumeration of possible sources of Headers to apply filters. +enum HeaderType { + // Unspecified header, default value. + HEADER_UNSPECIFIED = 0; + + // Filter request headers + REQUEST = 1; + + // Filter object headers + OBJECT = 2; + + // Filter service headers. These are not processed by NeoFS nodes and + // exist for service use only. + SERVICE = 3; +} + +// Describes a single eACL rule. +message EACLRecord { + // NeoFS request Verb to match + Operation operation = 1 [ json_name = "operation" ]; + + // Rule execution result. Either allows or denies access if filters match. + Action action = 2 [ json_name = "action" ]; + + // Filter to check particular properties of the request or the object. + // + // By default `key` field refers to the corresponding object's `Attribute`. + // Some Object's header fields can also be accessed by adding `$Object:` + // prefix to the name. Here is the list of fields available via this prefix: + // + // * $Object:version \ + // version + // * $Object:objectID \ + // object_id + // * $Object:containerID \ + // container_id + // * $Object:ownerID \ + // owner_id + // * $Object:creationEpoch \ + // creation_epoch + // * $Object:payloadLength \ + // payload_length + // * $Object:payloadHash \ + // payload_hash + // * $Object:objectType \ + // object_type + // * $Object:homomorphicHash \ + // homomorphic_hash + // + // Please note, that if request or response does not have object's headers of + // full object (Range, RangeHash, Search, Delete), it will not be possible to + // filter by object header fields or user attributes. From the well-known list + // only `$Object:objectID` and `$Object:containerID` will be available, as + // it's possible to take that information from the requested address. + message Filter { + // Define if Object or Request header will be used + HeaderType header_type = 1 [ json_name = "headerType" ]; + + // Match operation type + MatchType match_type = 2 [ json_name = "matchType" ]; + + // Name of the Header to use + string key = 3 [ json_name = "key" ]; + + // Expected Header Value or pattern to match + string value = 4 [ json_name = "value" ]; + } + + // List of filters to match and see if rule is applicable + repeated Filter filters = 3 [ json_name = "filters" ]; + + // Target to apply ACL rule. Can be a subject's role class or a list of public + // keys to match. + message Target { + // Target subject's role class + Role role = 1 [ json_name = "role" ]; + + // List of public keys to identify target subject + repeated bytes keys = 2 [ json_name = "keys" ]; + } + // List of target subjects to apply ACL rule to + repeated Target targets = 4 [ json_name = "targets" ]; +} + +// Extended ACL rules table. A list of ACL rules defined additionally to Basic +// ACL. Extended ACL rules can be attached to a container and can be updated +// or may be defined in `BearerToken` structure. Please see the corresponding +// NeoFS Technical Specification section for detailed description. +message EACLTable { + // eACL format version. Effectively, the version of API library used to create + // eACL Table. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Identifier of the container that should use given access control rules + neo.fs.v2.refs.ContainerID container_id = 2 [ json_name = "containerID" ]; + + // List of Extended ACL rules + repeated EACLRecord records = 3 [ json_name = "records" ]; +} + +// BearerToken allows to attach signed Extended ACL rules to the request in +// `RequestMetaHeader`. If container's Basic ACL rules allow, the attached rule +// set will be checked instead of one attached to the container itself. Just +// like [JWT](https://jwt.io), it has a limited lifetime and scope, hence can be +// used in the similar use cases, like providing authorisation to externally +// authenticated party. +// +// BearerToken can be issued only by the container's owner and must be signed +// using the key associated with the container's `OwnerID`. +message BearerToken { + // Bearer Token body structure contains Extended ACL table issued by the + // container owner with additional information preventing token abuse. + message Body { + // Table of Extended ACL rules to use instead of the ones attached to the + // container. If it contains `container_id` field, bearer token is only + // valid for this specific container. Otherwise, any container of the same + // owner is allowed. + EACLTable eacl_table = 1 [ json_name = "eaclTable" ]; + + // `OwnerID` defines to whom the token was issued. It must match the request + // originator's `OwnerID`. If empty, any token bearer will be accepted. + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Lifetime parameters of the token. Field names taken from + // [rfc7519](https://tools.ietf.org/html/rfc7519). + message TokenLifetime { + // Expiration Epoch + uint64 exp = 1 [ json_name = "exp" ]; + + // Not valid before Epoch + uint64 nbf = 2 [ json_name = "nbf" ]; + + // Issued at Epoch + uint64 iat = 3 [ json_name = "iat" ]; + } + // Token expiration and valid time period parameters + TokenLifetime lifetime = 3 [ json_name = "lifetime" ]; + + // AllowImpersonate flag to consider token signer as request owner. + // If this field is true extended ACL table in token body isn't processed. + bool allow_impersonate = 4 [ json_name = "allowImpersonate" ]; + } + // Bearer Token body + Body body = 1 [ json_name = "body" ]; + + // Signature of BearerToken body + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/apemanager/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/apemanager/service.proto new file mode 100644 index 0000000..6b9da60 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/apemanager/service.proto @@ -0,0 +1,171 @@ +syntax = "proto3"; + +package frostfs.v2.apemanager; + +import "apemanager/types.proto"; +import "session/types.proto"; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; + +// `APEManagerService` provides API to manage rule chains within sidechain's +// `Policy` smart contract. +service APEManagerService { + // Add a rule chain for a specific target to `Policy` smart contract. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // the chain has been successfully added; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc AddChain(AddChainRequest) returns (AddChainResponse); + + // Remove a rule chain for a specific target from `Policy` smart contract. + // RemoveChain is an idempotent operation: removal of non-existing rule chain + // also means success. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // the chain has been successfully removed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc RemoveChain(RemoveChainRequest) returns (RemoveChainResponse); + + // List chains defined for a specific target from `Policy` smart contract. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // chains have been successfully listed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container (as target) not found; + // - **APE_MANAGER_ACCESS_DENIED** (5120, SECTION_APE_MANAGER): \ + // the operation is denied by the service. + rpc ListChains(ListChainsRequest) returns (ListChainsResponse); +} + +message AddChainRequest { + message Body { + // A target for which a rule chain is added. + ChainTarget target = 1; + + // The chain to set for the target. + Chain chain = 2; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message AddChainResponse { + message Body { + // Chain ID assigned for the added rule chain. + // If chain ID is left empty in the request, then + // it will be generated. + bytes chain_id = 1; + } + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +message RemoveChainRequest { + message Body { + // Target for which a rule chain is removed. + ChainTarget target = 1; + + // Chain ID assigned for the rule chain. + bytes chain_id = 2; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message RemoveChainResponse { + // Since RemoveChain is an idempotent operation, then the only indicator that + // operation could not be performed is an error returning to a client. + message Body {} + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +message ListChainsRequest { + message Body { + // Target for which rule chains are listed. + ChainTarget target = 1; + } + + // The request's body. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +message ListChainsResponse { + message Body { + // The list of chains defined for the reqeusted target. + repeated Chain chains = 1; + } + + // The response's body. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ProtosV2/apemanager/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/apemanager/types.proto new file mode 100644 index 0000000..c064627 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/apemanager/types.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package frostfs.v2.apemanager; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager/grpc;apemanager"; + +// TargetType is a type target to which a rule chain is defined. +enum TargetType { + UNDEFINED = 0; + + NAMESPACE = 1; + + CONTAINER = 2; + + USER = 3; + + GROUP = 4; +} + +// ChainTarget is an object to which a rule chain is defined. +message ChainTarget { + TargetType type = 1; + + string name = 2; +} + +// Chain is a chain of rules defined for a specific target. +message Chain { + oneof kind { + // Raw representation of a serizalized rule chain. + bytes raw = 1; + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs b/sdk/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs new file mode 100644 index 0000000..fd6535c --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/container/Extension.Message.cs @@ -0,0 +1,397 @@ +using Google.Protobuf; +using FrostFS.Session; + +namespace FrostFS.Container +{ + public partial class AnnounceUsedSpaceRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class AnnounceUsedSpaceResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class PutResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class DeleteRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class DeleteResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class ListRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class ListResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class SetExtendedACLRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class SetExtendedACLResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetExtendedACLRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class GetExtendedACLResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/container/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/container/service.proto new file mode 100644 index 0000000..4a5cc02 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/container/service.proto @@ -0,0 +1,431 @@ +syntax = "proto3"; + +package neo.fs.v2.container; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container/grpc;container"; +option csharp_namespace = "FrostFS.Container"; + +import "acl/types.proto"; +import "container/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `ContainerService` provides API to interact with `Container` smart contract +// in NeoFS sidechain via other NeoFS nodes. All of those actions can be done +// equivalently by directly issuing transactions and RPC calls to sidechain +// nodes. +service ContainerService { + // `Put` invokes `Container` smart contract's `Put` method and returns + // response immediately. After a new block is issued in sidechain, request is + // verified by Inner Ring nodes. After one more block in sidechain, the + // container is added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to save the container has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container create access denied. + rpc Put(PutRequest) returns (PutResponse); + + // `Delete` invokes `Container` smart contract's `Delete` method and returns + // response immediately. After a new block is issued in sidechain, request is + // verified by Inner Ring nodes. After one more block in sidechain, the + // container is added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to remove the container has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container delete access denied. + rpc Delete(DeleteRequest) returns (DeleteResponse); + + // Returns container structure from `Container` smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // requested container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied. + rpc Get(GetRequest) returns (GetResponse); + + // Returns all owner's containers from 'Container` smart contract' storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container list has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // container list access denied. + rpc List(ListRequest) returns (ListResponse); + + // Invokes 'SetEACL' method of 'Container` smart contract and returns response + // immediately. After one more block in sidechain, changes in an Extended ACL + // are added into smart contract storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // request to save container eACL has been sent to the sidechain; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // set container eACL access denied. + rpc SetExtendedACL(SetExtendedACLRequest) returns (SetExtendedACLResponse); + + // Returns Extended ACL table and signature from `Container` smart contract + // storage. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // container eACL has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // container not found; + // - **EACL_NOT_FOUND** (3073, SECTION_CONTAINER): \ + // eACL table not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container eACL is denied. + rpc GetExtendedACL(GetExtendedACLRequest) returns (GetExtendedACLResponse); + + // Announces the space values used by the container for P2P synchronization. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // estimation of used space has been successfully announced; + // - Common failures (SECTION_FAILURE_COMMON). + rpc AnnounceUsedSpace(AnnounceUsedSpaceRequest) + returns (AnnounceUsedSpaceResponse); +} + +// New NeoFS Container creation request +message PutRequest { + // Container creation request has container structure's signature as a + // separate field. It's not stored in sidechain, just verified on container + // creation by `Container` smart contract. `ContainerID` is a SHA256 hash of + // the stable-marshalled container strucutre, hence there is no need for + // additional signature checks. + message Body { + // Container structure to register in NeoFS + container.Container container = 1; + + // Signature of a stable-marshalled container according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of container put request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// New NeoFS Container creation response +message PutResponse { + // Container put response body contains information about the newly registered + // container as seen by `Container` smart contract. `ContainerID` can be + // calculated beforehand from the container structure and compared to the one + // returned here to make sure everything has been done as expected. + message Body { + // Unique identifier of the newly created container + neo.fs.v2.refs.ContainerID container_id = 1; + } + // Body of container put response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Container removal request +message DeleteRequest { + // Container removal request body has signed `ContainerID` as a proof of + // the container owner's intent. The signature will be verified by `Container` + // smart contract, so signing algorithm must be supported by NeoVM. + message Body { + // Identifier of the container to delete from NeoFS + neo.fs.v2.refs.ContainerID container_id = 1; + + // `ContainerID` signed with the container owner's key according to + // RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of container delete request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// `DeleteResponse` has an empty body because delete operation is asynchronous +// and done via consensus in Inner Ring nodes. +message DeleteResponse { + // `DeleteResponse` has an empty body because delete operation is asynchronous + // and done via consensus in Inner Ring nodes. + message Body {} + // Body of container delete response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get container structure +message GetRequest { + // Get container structure request body. + message Body { + // Identifier of the container to get + neo.fs.v2.refs.ContainerID container_id = 1; + } + // Body of container get request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get container structure +message GetResponse { + // Get container response body does not have container structure signature. It + // has been already verified upon container creation. + message Body { + // Requested container structure + Container container = 1; + + // Signature of a stable-marshalled container according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + + // Session token if the container has been created within the session + neo.fs.v2.session.SessionToken session_token = 3; + } + // Body of container get response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// List containers +message ListRequest { + // List containers request body. + message Body { + // Identifier of the container owner + neo.fs.v2.refs.OwnerID owner_id = 1; + } + // Body of list containers request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// List containers +message ListResponse { + // List containers response body. + message Body { + // List of `ContainerID`s belonging to the requested `OwnerID` + repeated refs.ContainerID container_ids = 1; + } + + // Body of list containers response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Set Extended ACL +message SetExtendedACLRequest { + // Set Extended ACL request body does not have separate `ContainerID` + // reference. It will be taken from `EACLTable.container_id` field. + message Body { + // Extended ACL table to set for the container + neo.fs.v2.acl.EACLTable eacl = 1; + + // Signature of stable-marshalled Extended ACL table according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + } + // Body of set extended acl request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Set Extended ACL +message SetExtendedACLResponse { + // `SetExtendedACLResponse` has an empty body because the operation is + // asynchronous and the update should be reflected in `Container` smart + // contract's storage after next block is issued in sidechain. + message Body {} + + // Body of set extended acl response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get Extended ACL +message GetExtendedACLRequest { + // Get Extended ACL request body + message Body { + // Identifier of the container having Extended ACL + neo.fs.v2.refs.ContainerID container_id = 1; + } + + // Body of get extended acl request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get Extended ACL +message GetExtendedACLResponse { + // Get Extended ACL Response body can be empty if the requested container does + // not have Extended ACL Table attached or Extended ACL has not been allowed + // at the time of container creation. + message Body { + // Extended ACL requested, if available + neo.fs.v2.acl.EACLTable eacl = 1; + + // Signature of stable-marshalled Extended ACL according to RFC-6979. + neo.fs.v2.refs.SignatureRFC6979 signature = 2; + + // Session token if Extended ACL was set within a session + neo.fs.v2.session.SessionToken session_token = 3; + } + // Body of get extended acl response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Announce container used space +message AnnounceUsedSpaceRequest { + // Container used space announcement body. + message Body { + // Announcement contains used space information for a single container. + message Announcement { + // Epoch number for which the container size estimation was produced. + uint64 epoch = 1; + + // Identifier of the container. + neo.fs.v2.refs.ContainerID container_id = 2; + + // Used space is a sum of object payload sizes of a specified + // container, stored in the node. It must not include inhumed objects. + uint64 used_space = 3; + } + + // List of announcements. If nodes share several containers, + // announcements are transferred in a batch. + repeated Announcement announcements = 1; + } + + // Body of announce used space request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Announce container used space +message AnnounceUsedSpaceResponse { + // `AnnounceUsedSpaceResponse` has an empty body because announcements are + // one way communication. + message Body {} + + // Body of announce used space response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/container/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/container/types.proto new file mode 100644 index 0000000..075081a --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/container/types.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +package neo.fs.v2.container; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container/grpc;container"; +option csharp_namespace = "FrostFS.Container"; + +import "netmap/types.proto"; +import "refs/types.proto"; + +// Container is a structure that defines object placement behaviour. Objects can +// be stored only within containers. They define placement rule, attributes and +// access control information. An ID of a container is a 32 byte long SHA256 +// hash of stable-marshalled container message. +message Container { + // Container format version. Effectively, the version of API library used to + // create the container. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Identifier of the container owner + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Nonce is a 16 byte UUIDv4, used to avoid collisions of `ContainerID`s + bytes nonce = 3 [ json_name = "nonce" ]; + + // `BasicACL` contains access control rules for the owner, system and others + // groups, as well as permission bits for `BearerToken` and `Extended ACL` + uint32 basic_acl = 4 [ json_name = "basicACL" ]; + + // `Attribute` is a user-defined Key-Value metadata pair attached to the + // container. Container attributes are immutable. They are set at the moment + // of container creation and can never be added or updated. + // + // Key name must be a container-unique valid UTF-8 string. Value can't be + // empty. Containers with duplicated attribute names or attributes with empty + // values will be considered invalid. + // + // There are some "well-known" attributes affecting system behaviour: + // + // * [ __SYSTEM__NAME ] \ + // (`__NEOFS__NAME` is deprecated) \ + // String of a human-friendly container name registered as a domain in + // NNS contract. + // * [ __SYSTEM__ZONE ] \ + // (`__NEOFS__ZONE` is deprecated) \ + // String of a zone for `__SYSTEM__NAME` (`__NEOFS__NAME` is deprecated). + // Used as a TLD of a domain name in NNS contract. If no zone is specified, + // use default zone: `container`. + // * [ __SYSTEM__DISABLE_HOMOMORPHIC_HASHING ] \ + // (`__NEOFS__DISABLE_HOMOMORPHIC_HASHING` is deprecated) \ + // Disables homomorphic hashing for the container if the value equals "true" + // string. Any other values are interpreted as missing attribute. Container + // could be accepted in a NeoFS network only if the global network hashing + // configuration value corresponds with that attribute's value. After + // container inclusion, network setting is ignored. + // + // And some well-known attributes used by applications only: + // + // * Name \ + // Human-friendly name + // * Timestamp \ + // User-defined local time of container creation in Unix Timestamp format + message Attribute { + // Attribute name key + string key = 1 [ json_name = "key" ]; + + // Attribute value + string value = 2 [ json_name = "value" ]; + } + // Attributes represent immutable container's meta data + repeated Attribute attributes = 5 [ json_name = "attributes" ]; + + // Placement policy for the object inside the container + neo.fs.v2.netmap.PlacementPolicy placement_policy = 6 + [ json_name = "placementPolicy" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/lock/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/lock/types.proto new file mode 100644 index 0000000..ff79102 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/lock/types.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package neo.fs.v2.lock; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/lock/grpc;lock"; +option csharp_namespace = "FrostFS.Lock"; + +import "refs/types.proto"; + +// Lock objects protects a list of objects from being deleted. The lifetime of a +// lock object is limited similar to regular objects in +// `__SYSTEM__EXPIRATION_EPOCH` (`__NEOFS__EXPIRATION_EPOCH` is deprecated) +// attribute. Lock object MUST have expiration epoch. It is impossible to delete +// a lock object via ObjectService.Delete RPC call. +message Lock { + // List of objects to lock. Must not be empty or carry empty IDs. + // All members must be of the `REGULAR` type. + repeated neo.fs.v2.refs.ObjectID members = 1 [ json_name = "members" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs b/sdk/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs new file mode 100644 index 0000000..350cf74 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/netmap/Extension.Message.cs @@ -0,0 +1,117 @@ +using Google.Protobuf; +using FrostFS.Session; + +namespace FrostFS.Netmap +{ + public partial class LocalNodeInfoRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class LocalNodeInfoResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class NetworkInfoRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class NetworkInfoResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/netmap/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/netmap/service.proto new file mode 100644 index 0000000..8611d9a --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/netmap/service.proto @@ -0,0 +1,162 @@ +syntax = "proto3"; + +package neo.fs.v2.netmap; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap/grpc;netmap"; +option csharp_namespace = "FrostFS.Netmap"; + +import "netmap/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `NetmapService` provides methods to work with `Network Map` and the +// information required to build it. The resulting `Network Map` is stored in +// sidechain `Netmap` smart contract, while related information can be obtained +// from other NeoFS nodes. +service NetmapService { + // Get NodeInfo structure from the particular node directly. + // Node information can be taken from `Netmap` smart contract. In some cases, + // though, one may want to get recent information directly or to talk to the + // node not yet present in the `Network Map` to find out what API version can + // be used for further communication. This can be also used to check if a node + // is up and running. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the server has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc LocalNodeInfo(LocalNodeInfoRequest) returns (LocalNodeInfoResponse); + + // Read recent information about the NeoFS network. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the current network state has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc NetworkInfo(NetworkInfoRequest) returns (NetworkInfoResponse); + + // Returns network map snapshot of the current NeoFS epoch. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // information about the current network map has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON). + rpc NetmapSnapshot(NetmapSnapshotRequest) returns (NetmapSnapshotResponse); +} + +// Get NodeInfo structure directly from a particular node +message LocalNodeInfoRequest { + // LocalNodeInfo request body is empty. + message Body {} + // Body of the LocalNodeInfo request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Local Node Info, including API Version in use +message LocalNodeInfoResponse { + // Local Node Info, including API Version in use. + message Body { + // Latest NeoFS API version in use + neo.fs.v2.refs.Version version = 1; + + // NodeInfo structure with recent information from node itself + NodeInfo node_info = 2; + } + // Body of the balance response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get NetworkInfo structure with the network view from a particular node. +message NetworkInfoRequest { + // NetworkInfo request body is empty. + message Body {} + // Body of the NetworkInfo request message + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Response with NetworkInfo structure including current epoch and +// sidechain magic number. +message NetworkInfoResponse { + // Information about the network. + message Body { + // NetworkInfo structure with recent information. + NetworkInfo network_info = 1; + } + // Body of the NetworkInfo response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get netmap snapshot request +message NetmapSnapshotRequest { + // Get netmap snapshot request body. + message Body {} + + // Body of get netmap snapshot request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Response with current netmap snapshot +message NetmapSnapshotResponse { + // Get netmap snapshot response body + message Body { + // Structure of the requested network map. + Netmap netmap = 1 [ json_name = "netmap" ]; + } + + // Body of get netmap snapshot response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect response execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/netmap/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/netmap/types.proto new file mode 100644 index 0000000..baaca04 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/netmap/types.proto @@ -0,0 +1,323 @@ +syntax = "proto3"; + +package neo.fs.v2.netmap; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/netmap/grpc;netmap"; +option csharp_namespace = "FrostFS.Netmap"; + +// Operations on filters +enum Operation { + // No Operation defined + OPERATION_UNSPECIFIED = 0; + + // Equal + EQ = 1; + + // Not Equal + NE = 2; + + // Greater then + GT = 3; + + // Greater or equal + GE = 4; + + // Less then + LT = 5; + + // Less or equal + LE = 6; + + // Logical OR + OR = 7; + + // Logical AND + AND = 8; + + // Logical negation + NOT = 9; +} + +// Selector modifier shows how the node set will be formed. By default selector +// just groups nodes into a bucket by attribute, selecting nodes only by their +// hash distance. +enum Clause { + // No modifier defined. Nodes will be selected from the bucket randomly + CLAUSE_UNSPECIFIED = 0; + + // SAME will select only nodes having the same value of bucket attribute + SAME = 1; + + // DISTINCT will select nodes having different values of bucket attribute + DISTINCT = 2; +} + +// This filter will return the subset of nodes from `NetworkMap` or another +// filter's results that will satisfy filter's conditions. +message Filter { + // Name of the filter or a reference to a named filter. '*' means + // application to the whole unfiltered NetworkMap. At top level it's used as a + // filter name. At lower levels it's considered to be a reference to another + // named filter + string name = 1 [ json_name = "name" ]; + + // Key to filter + string key = 2 [ json_name = "key" ]; + + // Filtering operation + Operation op = 3 [ json_name = "op" ]; + + // Value to match + string value = 4 [ json_name = "value" ]; + + // List of inner filters. Top level operation will be applied to the whole + // list. + repeated Filter filters = 5 [ json_name = "filters" ]; +} + +// Selector chooses a number of nodes from the bucket taking the nearest nodes +// to the provided `ContainerID` by hash distance. +message Selector { + // Selector name to reference in object placement section + string name = 1 [ json_name = "name" ]; + + // How many nodes to select from the bucket + uint32 count = 2 [ json_name = "count" ]; + + // Selector modifier showing how to form a bucket + Clause clause = 3 [ json_name = "clause" ]; + + // Bucket attribute to select from + string attribute = 4 [ json_name = "attribute" ]; + + // Filter reference to select from + string filter = 5 [ json_name = "filter" ]; +} + +// Number of object replicas in a set of nodes from the defined selector. If no +// selector set, the root bucket containing all possible nodes will be used by +// default. +message Replica { + // How many object replicas to put + uint32 count = 1 [ json_name = "count" ]; + + // Named selector bucket to put replicas + string selector = 2 [ json_name = "selector" ]; + + // Data shards count + uint32 ec_data_count = 3 [ json_name = "ecDataCount" ]; + + // Parity shards count + uint32 ec_parity_count = 4 [ json_name = "ecParityCount" ]; +} + +// Set of rules to select a subset of nodes from `NetworkMap` able to store +// container's objects. The format is simple enough to transpile from different +// storage policy definition languages. +message PlacementPolicy { + // Rules to set number of object replicas and place each one into a named + // bucket + repeated Replica replicas = 1 [ json_name = "replicas" ]; + + // Container backup factor controls how deep NeoFS will search for nodes + // alternatives to include into container's nodes subset + uint32 container_backup_factor = 2 [ json_name = "containerBackupFactor" ]; + + // Set of Selectors to form the container's nodes subset + repeated Selector selectors = 3 [ json_name = "selectors" ]; + + // List of named filters to reference in selectors + repeated Filter filters = 4 [ json_name = "filters" ]; + + // Unique flag defines non-overlapping application for replicas + bool unique = 5 [ json_name = "unique" ]; +} + +// NeoFS node description +message NodeInfo { + // Public key of the NeoFS node in a binary format + bytes public_key = 1 [ json_name = "publicKey" ]; + + // Ways to connect to a node + repeated string addresses = 2 [ json_name = "addresses" ]; + + // Administrator-defined Attributes of the NeoFS Storage Node. + // + // `Attribute` is a Key-Value metadata pair. Key name must be a valid UTF-8 + // string. Value can't be empty. + // + // Attributes can be constructed into a chain of attributes: any attribute can + // have a parent attribute and a child attribute (except the first and the + // last one). A string representation of the chain of attributes in NeoFS + // Storage Node configuration uses ":" and "/" symbols, e.g.: + // + // `NEOFS_NODE_ATTRIBUTE_1=key1:val1/key2:val2` + // + // Therefore the string attribute representation in the Node configuration + // must use "\:", "\/" and "\\" escaped symbols if any of them appears in an + // attribute's key or value. + // + // Node's attributes are mostly used during Storage Policy evaluation to + // calculate object's placement and find a set of nodes satisfying policy + // requirements. There are some "well-known" node attributes common to all the + // Storage Nodes in the network and used implicitly with default values if not + // explicitly set: + // + // * Capacity \ + // Total available disk space in Gigabytes. + // * Price \ + // Price in GAS tokens for storing one GB of data during one Epoch. In node + // attributes it's a string presenting floating point number with comma or + // point delimiter for decimal part. In the Network Map it will be saved as + // 64-bit unsigned integer representing number of minimal token fractions. + // * UN-LOCODE \ + // Node's geographic location in + // [UN/LOCODE](https://www.unece.org/cefact/codesfortrade/codes_index.html) + // format approximated to the nearest point defined in the standard. + // * CountryCode \ + // Country code in + // [ISO 3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) + // format. Calculated automatically from `UN-LOCODE` attribute. + // * Country \ + // Country short name in English, as defined in + // [ISO-3166](https://www.iso.org/obp/ui/#search). Calculated automatically + // from `UN-LOCODE` attribute. + // * Location \ + // Place names are given, whenever possible, in their national language + // versions as expressed in the Roman alphabet using the 26 characters of + // the character set adopted for international trade data interchange, + // written without diacritics . Calculated automatically from `UN-LOCODE` + // attribute. + // * SubDivCode \ + // Country's administrative subdivision where node is located. Calculated + // automatically from `UN-LOCODE` attribute based on `SubDiv` field. + // Presented in [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2) + // format. + // * SubDiv \ + // Country's administrative subdivision name, as defined in + // [ISO 3166-2](https://en.wikipedia.org/wiki/ISO_3166-2). Calculated + // automatically from `UN-LOCODE` attribute. + // * Continent \ + // Node's continent name according to the [Seven-Continent model] + // (https://en.wikipedia.org/wiki/Continent#Number). Calculated + // automatically from `UN-LOCODE` attribute. + // * ExternalAddr + // Node's preferred way for communications with external clients. + // Clients SHOULD use these addresses if possible. + // Must contain a comma-separated list of multi-addresses. + // + // For detailed description of each well-known attribute please see the + // corresponding section in NeoFS Technical Specification. + message Attribute { + // Key of the node attribute + string key = 1 [ json_name = "key" ]; + + // Value of the node attribute + string value = 2 [ json_name = "value" ]; + + // Parent keys, if any. For example for `City` it could be `Region` and + // `Country`. + repeated string parents = 3 [ json_name = "parents" ]; + } + // Carries list of the NeoFS node attributes in a key-value form. Key name + // must be a node-unique valid UTF-8 string. Value can't be empty. NodeInfo + // structures with duplicated attribute names or attributes with empty values + // will be considered invalid. + repeated Attribute attributes = 3 [ json_name = "attributes" ]; + + // Represents the enumeration of various states of the NeoFS node. + enum State { + // Unknown state + UNSPECIFIED = 0; + + // Active state in the network + ONLINE = 1; + + // Network unavailable state + OFFLINE = 2; + + // Maintenance state + MAINTENANCE = 3; + } + + // Carries state of the NeoFS node + State state = 4 [ json_name = "state" ]; +} + +// Network map structure +message Netmap { + // Network map revision number. + uint64 epoch = 1 [ json_name = "epoch" ]; + + // Nodes presented in network. + repeated NodeInfo nodes = 2 [ json_name = "nodes" ]; +} + +// NeoFS network configuration +message NetworkConfig { + // Single configuration parameter. Key MUST be network-unique. + // + // System parameters: + // - **AuditFee** \ + // Fee paid by the storage group owner to the Inner Ring member. + // Value: little-endian integer. Default: 0. + // - **BasicIncomeRate** \ + // Cost of storing one gigabyte of data for a period of one epoch. Paid by + // container owner to container nodes. + // Value: little-endian integer. Default: 0. + // - **ContainerAliasFee** \ + // Fee paid for named container's creation by the container owner. + // Value: little-endian integer. Default: 0. + // - **ContainerFee** \ + // Fee paid for container creation by the container owner. + // Value: little-endian integer. Default: 0. + // - **EpochDuration** \ + // NeoFS epoch duration measured in Sidechain blocks. + // Value: little-endian integer. Default: 0. + // - **HomomorphicHashingDisabled** \ + // Flag of disabling the homomorphic hashing of objects' payload. + // Value: true if any byte != 0. Default: false. + // - **InnerRingCandidateFee** \ + // Fee for entrance to the Inner Ring paid by the candidate. + // Value: little-endian integer. Default: 0. + // - **MaintenanceModeAllowed** \ + // Flag allowing setting the MAINTENANCE state to storage nodes. + // Value: true if any byte != 0. Default: false. + // - **MaxObjectSize** \ + // Maximum size of physically stored NeoFS object measured in bytes. + // Value: little-endian integer. Default: 0. + // - **WithdrawFee** \ + // Fee paid for withdrawal of funds paid by the account owner. + // Value: little-endian integer. Default: 0. + // - **MaxECDataCount** \ + // Maximum number of data shards for EC placement policy. + // Value: little-endian integer. Default: 0. + // - **MaxECParityCount** \ + // Maximum number of parity shards for EC placement policy. + // Value: little-endian integer. Default: 0. + message Parameter { + // Parameter key. UTF-8 encoded string + bytes key = 1 [ json_name = "key" ]; + + // Parameter value + bytes value = 2 [ json_name = "value" ]; + } + // List of parameter values + repeated Parameter parameters = 1 [ json_name = "parameters" ]; +} + +// Information about NeoFS network +message NetworkInfo { + // Number of the current epoch in the NeoFS network + uint64 current_epoch = 1 [ json_name = "currentEpoch" ]; + + // Magic number of the sidechain of the NeoFS network + uint64 magic_number = 2 [ json_name = "magicNumber" ]; + + // MillisecondsPerBlock network parameter of the sidechain of the NeoFS + // network + int64 ms_per_block = 3 [ json_name = "msPerBlock" ]; + + // NeoFS network configuration + NetworkConfig network_config = 4 [ json_name = "networkConfig" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/object/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/object/service.proto new file mode 100644 index 0000000..383e83b --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/object/service.proto @@ -0,0 +1,816 @@ +syntax = "proto3"; + +package neo.fs.v2.object; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object/grpc;object"; +option csharp_namespace = "FrostFS.Object"; + +import "object/types.proto"; +import "refs/types.proto"; +import "session/types.proto"; + +// `ObjectService` provides API for manipulating objects. Object operations do +// not affect the sidechain and are only served by nodes in p2p style. +service ObjectService { + // Receive full object structure, including Headers and payload. Response uses + // gRPC stream. First response message carries the object with the requested + // address. Chunk messages are parts of the object's payload if it is needed. + // All messages, except the first one, carry payload chunks. The requested + // object can be restored by concatenation of object message payload and all + // chunks keeping the receiving order. + // + // Extended headers can change `Get` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requsted version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions (starting from `__SYSTEM__NETMAP_EPOCH` + // (`__NEOFS__NETMAP_EPOCH` is deprecated) if specified or the latest one + // otherwise) of Network Map to find an object until the depth limit is + // reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // read access to the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Get(GetRequest) returns (stream GetResponse); + + // Put the object into container. Request uses gRPC stream. First message + // SHOULD be of PutHeader type. `ContainerID` and `OwnerID` of an object + // SHOULD be set. Session token SHOULD be obtained before `PUT` operation (see + // session package). Chunk messages are considered by server as a part of an + // object payload. All messages, except first one, SHOULD be payload chunks. + // Chunk messages SHOULD be sent in the direct order of fragmentation. + // + // Extended headers can change `Put` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requsted version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully saved in the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // write access to the container is denied; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // placement of an object of type TOMBSTONE that includes at least one + // locked object is prohibited; + // - **LOCK_NON_REGULAR_OBJECT** (2051, SECTION_OBJECT): \ + // placement of an object of type LOCK that includes at least one object of + // type other than REGULAR is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object storage container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_NOT_FOUND** (4096, SECTION_SESSION): \ + // (for trusted object preparation) session private key does not exist or + // has + // been deleted; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Put(stream PutRequest) returns (PutResponse); + + // Delete the object from a container. There is no immediate removal + // guarantee. Object will be marked for removal and deleted eventually. + // + // Extended headers can change `Delete` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully marked to be removed from the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // delete access to the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // the object could not be deleted because it has not been \ + // found within the container; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // deleting a locked object is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Delete(DeleteRequest) returns (DeleteResponse); + + // Returns the object Headers without data payload. By default full header is + // returned. If `main_only` request field is set, the short header with only + // the very minimal information will be returned instead. + // + // Extended headers can change `Head` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object header has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation HEAD of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Head(HeadRequest) returns (HeadResponse); + + // Search objects in container. Search query allows to match by Object + // Header's filed values. Please see the corresponding NeoFS Technical + // Specification section for more details. + // + // Extended headers can change `Search` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // objects have been successfully selected; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation SEARCH of the object is denied; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // search container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc Search(SearchRequest) returns (stream SearchResponse); + + // Get byte range of data payload. Range is set as an (offset, length) tuple. + // Like in `Get` method, the response uses gRPC stream. Requested range can be + // restored by concatenation of all received payload chunks keeping the + // receiving order. + // + // Extended headers can change `GetRange` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions of Network Map to find an object until the depth + // limit is reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // data range of the object payload has been successfully read; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation RANGE of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OBJECT_ALREADY_REMOVED** (2052, SECTION_OBJECT): \ + // the requested object has been marked as deleted. + // - **OUT_OF_RANGE** (2053, SECTION_OBJECT): \ + // the requested range is out of bounds; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc GetRange(GetRangeRequest) returns (stream GetRangeResponse); + + // Returns homomorphic or regular hash of object's payload range after + // applying XOR operation with the provided `salt`. Ranges are set of (offset, + // length) tuples. Hashes order in response corresponds to the ranges order in + // the request. Note that hash is calculated for XORed data. + // + // Extended headers can change `GetRangeHash` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH ] \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ + // (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ + // Will try older versions of Network Map to find an object until the depth + // limit is reached. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // data range of the object payload has been successfully hashed; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // access to operation RANGEHASH of the object is denied; + // - **OBJECT_NOT_FOUND** (2049, SECTION_OBJECT): \ + // object not found in container; + // - **OUT_OF_RANGE** (2053, SECTION_OBJECT): \ + // the requested range is out of bounds; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc GetRangeHash(GetRangeHashRequest) returns (GetRangeHashResponse); + + // Put the prepared object into container. + // `ContainerID`, `ObjectID`, `OwnerID`, `PayloadHash` and `PayloadLength` of + // an object MUST be set. + // + // Extended headers can change `Put` behaviour: + // * [ __SYSTEM__NETMAP_EPOCH \ + // (`__NEOFS__NETMAP_EPOCH` is deprecated) \ + // Will use the requested version of Network Map for object placement + // calculation. + // + // Please refer to detailed `XHeader` description. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): \ + // object has been successfully saved in the container; + // - Common failures (SECTION_FAILURE_COMMON); + // - **ACCESS_DENIED** (2048, SECTION_OBJECT): \ + // write access to the container is denied; + // - **LOCKED** (2050, SECTION_OBJECT): \ + // placement of an object of type TOMBSTONE that includes at least one + // locked object is prohibited; + // - **LOCK_NON_REGULAR_OBJECT** (2051, SECTION_OBJECT): \ + // placement of an object of type LOCK that includes at least one object of + // type other than REGULAR is prohibited; + // - **CONTAINER_NOT_FOUND** (3072, SECTION_CONTAINER): \ + // object storage container not found; + // - **CONTAINER_ACCESS_DENIED** (3074, SECTION_CONTAINER): \ + // access to container is denied; + // - **TOKEN_NOT_FOUND** (4096, SECTION_SESSION): \ + // (for trusted object preparation) session private key does not exist or + // has + // been deleted; + // - **TOKEN_EXPIRED** (4097, SECTION_SESSION): \ + // provided session token has expired. + rpc PutSingle(PutSingleRequest) returns (PutSingleResponse); +} + +// GET object request +message GetRequest { + // GET Object request body + message Body { + // Address of the requested object + neo.fs.v2.refs.Address address = 1; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node + bool raw = 2; + } + // Body of get object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// GET object response +message GetResponse { + // GET Object Response body + message Body { + // Initial part of the `Object` structure stream. Technically it's a + // set of all `Object` structure's fields except `payload`. + message Init { + // Object's unique identifier. + neo.fs.v2.refs.ObjectID object_id = 1; + + // Signed `ObjectID` + neo.fs.v2.refs.Signature signature = 2; + + // Object metadata headers + Header header = 3; + } + // Single message in the response stream. + oneof object_part { + // Initial part of the object stream + Init init = 1; + + // Chunked object payload + bytes chunk = 2; + + // Meta information of split hierarchy for object assembly. + SplitInfo split_info = 3; + + // Meta information for EC object assembly. + ECInfo ec_info = 4; + } + } + // Body of get object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// PUT object request +message PutRequest { + // PUT request body + message Body { + // Newly created object structure parameters. If some optional parameters + // are not set, they will be calculated by a peer node. + message Init { + // ObjectID if available. + neo.fs.v2.refs.ObjectID object_id = 1; + + // Object signature if available + neo.fs.v2.refs.Signature signature = 2; + + // Object's Header + Header header = 3; + + // Number of copies of the object to store within the RPC call. By + // default, object is processed according to the container's placement + // policy. Can be one of: + // 1. A single number; applied to the whole request and is treated as + // a minimal number of nodes that must store an object to complete the + // request successfully. + // 2. An ordered array; every number is treated as a minimal number of + // nodes in a corresponding placement vector that must store an object + // to complete the request successfully. The length MUST equal the + // placement vectors number, otherwise request is considered malformed. + repeated uint32 copies_number = 4; + } + // Single message in the request stream. + oneof object_part { + // Initial part of the object stream + Init init = 1; + + // Chunked object payload + bytes chunk = 2; + } + } + // Body of put object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// PUT Object response +message PutResponse { + // PUT Object response body + message Body { + // Identifier of the saved object + neo.fs.v2.refs.ObjectID object_id = 1; + } + // Body of put object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object DELETE request +message DeleteRequest { + // Object DELETE request body + message Body { + // Address of the object to be deleted + neo.fs.v2.refs.Address address = 1; + } + // Body of delete object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// DeleteResponse body is empty because we cannot guarantee permanent object +// removal in distributed system. +message DeleteResponse { + // Object DELETE Response has an empty body. + message Body { + // Address of the tombstone created for the deleted object + neo.fs.v2.refs.Address tombstone = 1; + } + + // Body of delete object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object HEAD request +message HeadRequest { + // Object HEAD request body + message Body { + // Address of the object with the requested Header + neo.fs.v2.refs.Address address = 1; + + // Return only minimal header subset + bool main_only = 2; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node + bool raw = 3; + } + // Body of head object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Tuple of a full object header and signature of an `ObjectID`. \ +// Signed `ObjectID` is present to verify full header's authenticity through the +// following steps: +// +// 1. Calculate `SHA-256` of the marshalled `Header` structure +// 2. Check if the resulting hash matches `ObjectID` +// 3. Check if `ObjectID` signature in `signature` field is correct +message HeaderWithSignature { + // Full object header + Header header = 1 [ json_name = "header" ]; + + // Signed `ObjectID` to verify full header's authenticity + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} + +// Object HEAD response +message HeadResponse { + // Object HEAD response body + message Body { + // Requested object header, it's part or meta information about split + // object. + oneof head { + // Full object's `Header` with `ObjectID` signature + HeaderWithSignature header = 1; + + // Short object header + ShortHeader short_header = 2; + + // Meta information of split hierarchy. + SplitInfo split_info = 3; + + // Meta information for EC object assembly. + ECInfo ec_info = 4; + } + } + // Body of head object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object Search request +message SearchRequest { + // Object Search request body + message Body { + // Container identifier were to search + neo.fs.v2.refs.ContainerID container_id = 1; + + // Version of the Query Language used + uint32 version = 2; + // Filter structure checks if the object header field or the attribute + // content matches a value. + // + // If no filters are set, search request will return all objects of the + // container, including Regular object and Tombstone + // objects. Most human users expect to get only object they can directly + // work with. In that case, `$Object:ROOT` filter should be used. + // + // By default `key` field refers to the corresponding object's `Attribute`. + // Some Object's header fields can also be accessed by adding `$Object:` + // prefix to the name. Here is the list of fields available via this prefix: + // + // * $Object:version \ + // version + // * $Object:objectID \ + // object_id + // * $Object:containerID \ + // container_id + // * $Object:ownerID \ + // owner_id + // * $Object:creationEpoch \ + // creation_epoch + // * $Object:payloadLength \ + // payload_length + // * $Object:payloadHash \ + // payload_hash + // * $Object:objectType \ + // object_type + // * $Object:homomorphicHash \ + // homomorphic_hash + // * $Object:split.parent \ + // object_id of parent + // * $Object:split.splitID \ + // 16 byte UUIDv4 used to identify the split object hierarchy parts + // + // There are some well-known filter aliases to match objects by certain + // properties: + // + // * $Object:ROOT \ + // Returns only `REGULAR` type objects that are not split or that are the + // top level root objects in a split hierarchy. This includes objects not + // present physically, like large objects split into smaller objects + // without a separate top-level root object. Objects of other types like + // Locks and Tombstones will not be shown. This filter may be + // useful for listing objects like `ls` command of some virtual file + // system. This filter is activated if the `key` exists, disregarding the + // value and matcher type. + // * $Object:PHY \ + // Returns only objects physically stored in the system. This filter is + // activated if the `key` exists, disregarding the value and matcher type. + // + // Note: using filters with a key with prefix `$Object:` and match type + // `NOT_PRESENT `is not recommended since this is not a cross-version + // approach. Behavior when processing this kind of filters is undefined. + message Filter { + // Match type to use + MatchType match_type = 1 [ json_name = "matchType" ]; + + // Attribute or Header fields to match + string key = 2 [ json_name = "key" ]; + + // Value to match + string value = 3 [ json_name = "value" ]; + } + // List of search expressions + repeated Filter filters = 3; + } + // Body of search object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Search response +message SearchResponse { + // Object Search response body + message Body { + // List of `ObjectID`s that match the search query + repeated neo.fs.v2.refs.ObjectID id_list = 1; + } + // Body of search object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object payload range.Ranges of zero length SHOULD be considered as invalid. +message Range { + // Offset of the range from the object payload start + uint64 offset = 1; + + // Length in bytes of the object payload range + uint64 length = 2; +} + +// Request part of object's payload +message GetRangeRequest { + // Byte range of object's payload request body + message Body { + // Address of the object containing the requested payload range + neo.fs.v2.refs.Address address = 1; + + // Requested payload range + Range range = 2; + + // If `raw` flag is set, request will work only with objects that are + // physically stored on the peer node. + bool raw = 3; + } + + // Body of get range object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get part of object's payload +message GetRangeResponse { + // Get Range response body uses streams to transfer the response. Because + // object payload considered a byte sequence, there is no need to have some + // initial preamble message. The requested byte range is sent as a series + // chunks. + message Body { + // Requested object range or meta information about split object. + oneof range_part { + // Chunked object payload's range. + bytes chunk = 1; + + // Meta information of split hierarchy. + SplitInfo split_info = 2; + + // Meta information for EC object assembly. + ECInfo ec_info = 3; + } + } + + // Body of get range object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Get hash of object's payload part +message GetRangeHashRequest { + // Get hash of object's payload part request body. + message Body { + // Address of the object that containing the requested payload range + neo.fs.v2.refs.Address address = 1; + + // List of object's payload ranges to calculate homomorphic hash + repeated Range ranges = 2; + + // Binary salt to XOR object's payload ranges before hash calculation + bytes salt = 3; + + // Checksum algorithm type + neo.fs.v2.refs.ChecksumType type = 4; + } + // Body of get range hash object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Get hash of object's payload part +message GetRangeHashResponse { + // Get hash of object's payload part response body. + message Body { + // Checksum algorithm type + neo.fs.v2.refs.ChecksumType type = 1; + + // List of range hashes in a binary format + repeated bytes hash_list = 2; + } + // Body of get range hash object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} + +// Object PUT Single request +message PutSingleRequest { + // PUT Single request body + message Body { + // Prepared object with payload. + Object object = 1; + // Number of copies of the object to store within the RPC call. By default, + // object is processed according to the container's placement policy. + // Every number is treated as a minimal number of + // nodes in a corresponding placement vector that must store an object + // to complete the request successfully. The length MUST equal the placement + // vectors number, otherwise request is considered malformed. + repeated uint32 copies_number = 2; + } + // Body of put single object request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Object PUT Single response +message PutSingleResponse { + // PUT Single Object response body + message Body {} + // Body of put single object response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ProtosV2/object/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/object/types.proto new file mode 100644 index 0000000..6e62b86 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/object/types.proto @@ -0,0 +1,266 @@ +syntax = "proto3"; + +package neo.fs.v2.object; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/object/grpc;object"; +option csharp_namespace = "FrostFS.Object"; + +import "refs/types.proto"; +import "session/types.proto"; + +// Type of the object payload content. Only `REGULAR` type objects can be split, +// hence `TOMBSTONE` and `LOCK` payload is limited by the +// maximum object size. +// +// String presentation of object type is the same as definition: +// * REGULAR +// * TOMBSTONE +// * LOCK +enum ObjectType { + // Just a normal object + REGULAR = 0; + + // Used internally to identify deleted objects + TOMBSTONE = 1; + + // Unused (previously storageGroup information) + // _ = 2; + + // Object lock + LOCK = 3; +} + +// Type of match expression +enum MatchType { + // Unknown. Not used + MATCH_TYPE_UNSPECIFIED = 0; + + // Full string match + STRING_EQUAL = 1; + + // Full string mismatch + STRING_NOT_EQUAL = 2; + + // Lack of key + NOT_PRESENT = 3; + + // String prefix match + COMMON_PREFIX = 4; +} + +// Short header fields +message ShortHeader { + // Object format version. Effectively, the version of API library used to + // create particular object. + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Epoch when the object was created + uint64 creation_epoch = 2 [ json_name = "creationEpoch" ]; + + // Object's owner + neo.fs.v2.refs.OwnerID owner_id = 3 [ json_name = "ownerID" ]; + + // Type of the object payload content + ObjectType object_type = 4 [ json_name = "objectType" ]; + + // Size of payload in bytes. + // `0xFFFFFFFFFFFFFFFF` means `payload_length` is unknown + uint64 payload_length = 5 [ json_name = "payloadLength" ]; + + // Hash of payload bytes + neo.fs.v2.refs.Checksum payload_hash = 6 [ json_name = "payloadHash" ]; + + // Homomorphic hash of the object payload + neo.fs.v2.refs.Checksum homomorphic_hash = 7 + [ json_name = "homomorphicHash" ]; +} + +// Object Header +message Header { + // Object format version. Effectively, the version of API library used to + // create particular object + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Object's container + neo.fs.v2.refs.ContainerID container_id = 2 [ json_name = "containerID" ]; + + // Object's owner + neo.fs.v2.refs.OwnerID owner_id = 3 [ json_name = "ownerID" ]; + + // Object creation Epoch + uint64 creation_epoch = 4 [ json_name = "creationEpoch" ]; + + // Size of payload in bytes. + // `0xFFFFFFFFFFFFFFFF` means `payload_length` is unknown. + uint64 payload_length = 5 [ json_name = "payloadLength" ]; + + // Hash of payload bytes + neo.fs.v2.refs.Checksum payload_hash = 6 [ json_name = "payloadHash" ]; + + // Type of the object payload content + ObjectType object_type = 7 [ json_name = "objectType" ]; + + // Homomorphic hash of the object payload + neo.fs.v2.refs.Checksum homomorphic_hash = 8 + [ json_name = "homomorphicHash" ]; + + // Session token, if it was used during Object creation. Need it to verify + // integrity and authenticity out of Request scope. + neo.fs.v2.session.SessionToken session_token = 9 + [ json_name = "sessionToken" ]; + + // `Attribute` is a user-defined Key-Value metadata pair attached to an + // object. + // + // Key name must be an object-unique valid UTF-8 string. Value can't be empty. + // Objects with duplicated attribute names or attributes with empty values + // will be considered invalid. + // + // There are some "well-known" attributes starting with `__SYSTEM__` + // (`__NEOFS__` is deprecated) prefix that affect system behaviour: + // + // * [ __SYSTEM__UPLOAD_ID ] \ + // (`__NEOFS__UPLOAD_ID` is deprecated) \ + // Marks smaller parts of a split bigger object + // * [ __SYSTEM__EXPIRATION_EPOCH ] \ + // (`__NEOFS__EXPIRATION_EPOCH` is deprecated) \ + // The epoch after which object with no LOCKs on it becomes unavailable. + // Locked object continues to be available until each of the LOCKs expire. + // * [ __SYSTEM__TICK_EPOCH ] \ + // (`__NEOFS__TICK_EPOCH` is deprecated) \ + // Decimal number that defines what epoch must produce + // object notification with UTF-8 object address in a + // body (`0` value produces notification right after + // object put) + // * [ __SYSTEM__TICK_TOPIC ] \ + // (`__NEOFS__TICK_TOPIC` is deprecated) \ + // UTF-8 string topic ID that is used for object notification + // + // And some well-known attributes used by applications only: + // + // * Name \ + // Human-friendly name + // * FileName \ + // File name to be associated with the object on saving + // * FilePath \ + // Full path to be associated with the object on saving. Should start with a + // '/' and use '/' as a delimiting symbol. Trailing '/' should be + // interpreted as a virtual directory marker. If an object has conflicting + // FilePath and FileName, FilePath should have higher priority, because it + // is used to construct the directory tree. FilePath with trailing '/' and + // non-empty FileName attribute should not be used together. + // * Timestamp \ + // User-defined local time of object creation in Unix Timestamp format + // * Content-Type \ + // MIME Content Type of object's payload + // + // For detailed description of each well-known attribute please see the + // corresponding section in NeoFS Technical Specification. + message Attribute { + // string key to the object attribute + string key = 1 [ json_name = "key" ]; + // string value of the object attribute + string value = 2 [ json_name = "value" ]; + } + // User-defined object attributes + repeated Attribute attributes = 10 [ json_name = "attributes" ]; + + // Bigger objects can be split into a chain of smaller objects. Information + // about inter-dependencies between spawned objects and how to re-construct + // the original one is in the `Split` headers. Parent and children objects + // must be within the same container. + message Split { + // Identifier of the origin object. Known only to the minor child. + neo.fs.v2.refs.ObjectID parent = 1 [ json_name = "parent" ]; + + // Identifier of the left split neighbor + neo.fs.v2.refs.ObjectID previous = 2 [ json_name = "previous" ]; + + // `signature` field of the parent object. Used to reconstruct parent. + neo.fs.v2.refs.Signature parent_signature = 3 + [ json_name = "parentSignature" ]; + + // `header` field of the parent object. Used to reconstruct parent. + Header parent_header = 4 [ json_name = "parentHeader" ]; + + // List of identifiers of the objects generated by splitting current one. + repeated neo.fs.v2.refs.ObjectID children = 5 [ json_name = "children" ]; + + // 16 byte UUIDv4 used to identify the split object hierarchy parts. Must be + // unique inside container. All objects participating in the split must have + // the same `split_id` value. + bytes split_id = 6 [ json_name = "splitID" ]; + } + // Position of the object in the split hierarchy + Split split = 11 [ json_name = "split" ]; + + // Erasure code can be applied to any object. + // Information about encoded object structure is stored in `EC` header. + // All objects belonging to a single EC group have the same `parent` field. + message EC { + // Identifier of the origin object. Known to all chunks. + neo.fs.v2.refs.ObjectID parent = 1 [ json_name = "parent" ]; + // Index of this chunk. + uint32 index = 2 [ json_name = "index" ]; + // Total number of chunks in this split. + uint32 total = 3 [ json_name = "total" ]; + // Total length of a parent header. Used to trim padding zeroes. + uint32 header_length = 4 [ json_name = "headerLength" ]; + // Chunk of a parent header. + bytes header = 5 [ json_name = "header" ]; + } + // Erasure code chunk information. + EC ec = 12 [ json_name = "ec" ]; +} + +// Object structure. Object is immutable and content-addressed. It means +// `ObjectID` will change if the header or the payload changes. It's calculated +// as a hash of header field which contains hash of the object's payload. +// +// For non-regular object types payload format depends on object type specified +// in the header. +message Object { + // Object's unique identifier. + neo.fs.v2.refs.ObjectID object_id = 1 [ json_name = "objectID" ]; + + // Signed object_id + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; + + // Object metadata headers + Header header = 3 [ json_name = "header" ]; + + // Payload bytes + bytes payload = 4 [ json_name = "payload" ]; +} + +// Meta information of split hierarchy for object assembly. With the last part +// one can traverse linked list of split hierarchy back to the first part and +// assemble the original object. With a linking object one can assemble an +// object right from the object parts. +message SplitInfo { + // 16 byte UUID used to identify the split object hierarchy parts. + bytes split_id = 1; + + // The identifier of the last object in split hierarchy parts. It contains + // split header with the original object header. + neo.fs.v2.refs.ObjectID last_part = 2; + + // The identifier of a linking object for split hierarchy parts. It contains + // split header with the original object header and a sorted list of + // object parts. + neo.fs.v2.refs.ObjectID link = 3; +} + +// Meta information for the erasure-encoded object. +message ECInfo { + message Chunk { + // Object ID of the chunk. + neo.fs.v2.refs.ObjectID id = 1; + // Index of the chunk. + uint32 index = 2; + // Total number of chunks in this split. + uint32 total = 3; + } + // Chunk stored on the node. + repeated Chunk chunks = 1; +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ProtosV2/refs/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/refs/types.proto new file mode 100644 index 0000000..15d32c1 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/refs/types.proto @@ -0,0 +1,150 @@ +syntax = "proto3"; + +package neo.fs.v2.refs; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs/grpc;refs"; +option csharp_namespace = "FrostFS.Refs"; + +// Objects in NeoFS are addressed by their ContainerID and ObjectID. +// +// String presentation of `Address` is a concatenation of string encoded +// `ContainerID` and `ObjectID` delimited by '/' character. +message Address { + // Container identifier + ContainerID container_id = 1 [ json_name = "containerID" ]; + // Object identifier + ObjectID object_id = 2 [ json_name = "objectID" ]; +} + +// NeoFS Object unique identifier. Objects are immutable and content-addressed. +// It means `ObjectID` will change if the `header` or the `payload` changes. +// +// `ObjectID` is a 32 byte long +// [SHA256](https://csrc.nist.gov/publications/detail/fips/180/4/final) hash of +// the object's `header` field, which, in it's turn, contains the hash of the +// object's payload. +// +// String presentation is a +// [base58](https://tools.ietf.org/html/draft-msporny-base58-02) encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message ObjectID { + // Object identifier in a binary format + bytes value = 1 [ json_name = "value" ]; +} + +// NeoFS container identifier. Container structures are immutable and +// content-addressed. +// +// `ContainerID` is a 32 byte long +// [SHA256](https://csrc.nist.gov/publications/detail/fips/180/4/final) hash of +// stable-marshalled container message. +// +// String presentation is a +// [base58](https://tools.ietf.org/html/draft-msporny-base58-02) encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message ContainerID { + // Container identifier in a binary format. + bytes value = 1 [ json_name = "value" ]; +} + +// `OwnerID` is a derivative of a user's main public key. The transformation +// algorithm is the same as for Neo3 wallet addresses. Neo3 wallet address can +// be directly used as `OwnerID`. +// +// `OwnerID` is a 25 bytes sequence starting with Neo version prefix byte +// followed by 20 bytes of ScrptHash and 4 bytes of checksum. +// +// String presentation is a [Base58 +// Check](https://en.bitcoin.it/wiki/Base58Check_encoding) Encoded string. +// +// JSON value will be data encoded as a string using standard base64 +// encoding with paddings. Either +// [standard](https://tools.ietf.org/html/rfc4648#section-4) or +// [URL-safe](https://tools.ietf.org/html/rfc4648#section-5) base64 encoding +// with/without paddings are accepted. +message OwnerID { + // Identifier of the container owner in a binary format + bytes value = 1 [ json_name = "value" ]; +} + +// API version used by a node. +// +// String presentation is a Semantic Versioning 2.0.0 compatible version string +// with 'v' prefix. i.e. `vX.Y`, where `X` is the major number, `Y` is the minor +// number. +message Version { + // Major API version + uint32 major = 1 [ json_name = "major" ]; + + // Minor API version + uint32 minor = 2 [ json_name = "minor" ]; +} + +// Signature of something in NeoFS. +message Signature { + // Public key used for signing + bytes key = 1 [ json_name = "key" ]; + // Signature + bytes sign = 2 [ json_name = "signature" ]; + // Scheme contains digital signature scheme identifier + SignatureScheme scheme = 3 [ json_name = "scheme" ]; +} + +// Signature scheme describes digital signing scheme used for (key, signature) +// pair. +enum SignatureScheme { + // ECDSA with SHA-512 hashing (FIPS 186-3) + ECDSA_SHA512 = 0; + + // Deterministic ECDSA with SHA-256 hashing (RFC 6979) + ECDSA_RFC6979_SHA256 = 1; + + // Deterministic ECDSA with SHA-256 hashing using WalletConnect API. + // Here the algorithm is the same, but the message format differs. + ECDSA_RFC6979_SHA256_WALLET_CONNECT = 2; +} + +// RFC 6979 signature. +message SignatureRFC6979 { + // Public key used for signing + bytes key = 1 [ json_name = "key" ]; + // Deterministic ECDSA with SHA-256 hashing + bytes sign = 2 [ json_name = "signature" ]; +} + +// Checksum algorithm type. +enum ChecksumType { + // Unknown. Not used + CHECKSUM_TYPE_UNSPECIFIED = 0; + + // Tillich-Zemor homomorphic hash function + TZ = 1; + + // SHA-256 + SHA256 = 2; +} + +// Checksum message. +// Depending on checksum algorithm type, the string presentation may vary: +// +// * TZ \ +// Hex encoded string without `0x` prefix +// * SHA256 \ +// Hex encoded string without `0x` prefix +message Checksum { + // Checksum algorithm type + ChecksumType type = 1 [ json_name = "type" ]; + + // Checksum itself + bytes sum = 2 [ json_name = "sum" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs new file mode 100644 index 0000000..9261865 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.Message.cs @@ -0,0 +1,60 @@ +using Google.Protobuf; + +namespace FrostFS.Session +{ + public partial class CreateResponse : IResponse + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (ResponseMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (ResponseVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } + + public partial class CreateRequest : IRequest + { + IMetaHeader IVerificableMessage.GetMetaHeader() + { + return MetaHeader; + } + + IVerificationHeader IVerificableMessage.GetVerificationHeader() + { + return VerifyHeader; + } + + void IVerificableMessage.SetMetaHeader(IMetaHeader metaHeader) + { + MetaHeader = (RequestMetaHeader)metaHeader; + } + + void IVerificableMessage.SetVerificationHeader(IVerificationHeader verificationHeader) + { + VerifyHeader = (RequestVerificationHeader)verificationHeader; + } + + public IMessage GetBody() + { + return Body; + } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.MetaHeader.cs b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.MetaHeader.cs new file mode 100644 index 0000000..f653c7b --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.MetaHeader.cs @@ -0,0 +1,18 @@ +namespace FrostFS.Session +{ + public partial class RequestMetaHeader : IMetaHeader + { + public IMetaHeader GetOrigin() + { + return Origin; + } + } + + public partial class ResponseMetaHeader : IMetaHeader + { + public IMetaHeader GetOrigin() + { + return Origin; + } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.VerificationHeader.cs b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.VerificationHeader.cs new file mode 100644 index 0000000..8c994de --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.VerificationHeader.cs @@ -0,0 +1,28 @@ +namespace FrostFS.Session +{ + public partial class RequestVerificationHeader : IVerificationHeader + { + IVerificationHeader IVerificationHeader.GetOrigin() + { + return Origin; + } + + void IVerificationHeader.SetOrigin(IVerificationHeader verificationHeader) + { + Origin = (RequestVerificationHeader)verificationHeader; + } + } + + public partial class ResponseVerificationHeader : IVerificationHeader + { + IVerificationHeader IVerificationHeader.GetOrigin() + { + return Origin; + } + + void IVerificationHeader.SetOrigin(IVerificationHeader verificationHeader) + { + Origin = (ResponseVerificationHeader)verificationHeader; + } + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs new file mode 100644 index 0000000..a49be70 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/Extension.XHeader.cs @@ -0,0 +1,9 @@ +namespace FrostFS.Session +{ + public partial class XHeader + { + public const string ReservedXHeaderPrefix = "__NEOFS__"; + public const string XHeaderNetmapEpoch = ReservedXHeaderPrefix + "NETMAP_EPOCH"; + public const string XHeaderNetmapLookupDepth = ReservedXHeaderPrefix + "NETMAP_LOOKUP_DEPTH"; + } +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/service.proto b/sdk/src/FrostFS.SDK.ProtosV2/session/service.proto new file mode 100644 index 0000000..6f48e3a --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/service.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package neo.fs.v2.session; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session/grpc;session"; +option csharp_namespace = "FrostFS.Session"; + +import "refs/types.proto"; +import "session/types.proto"; + +// `SessionService` allows to establish a temporary trust relationship between +// two peer nodes and generate a `SessionToken` as the proof of trust to be +// attached in requests for further verification. Please see corresponding +// section of NeoFS Technical Specification for details. +service SessionService { + // Open a new session between two peers. + // + // Statuses: + // - **OK** (0, SECTION_SUCCESS): + // session has been successfully opened; + // - Common failures (SECTION_FAILURE_COMMON). + rpc Create(CreateRequest) returns (CreateResponse); +} + +// Information necessary for opening a session. +message CreateRequest { + // Session creation request body + message Body { + // Session initiating user's or node's key derived `OwnerID` + neo.fs.v2.refs.OwnerID owner_id = 1; + // Session expiration `Epoch` + uint64 expiration = 2; + } + // Body of a create session token request message. + Body body = 1; + + // Carries request meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.RequestMetaHeader meta_header = 2; + + // Carries request verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.RequestVerificationHeader verify_header = 3; +} + +// Information about the opened session. +message CreateResponse { + // Session creation response body + message Body { + // Identifier of a newly created session + bytes id = 1; + + // Public key used for session + bytes session_key = 2; + } + + // Body of create session token response message. + Body body = 1; + + // Carries response meta information. Header data is used only to regulate + // message transport and does not affect request execution. + neo.fs.v2.session.ResponseMetaHeader meta_header = 2; + + // Carries response verification information. This header is used to + // authenticate the nodes of the message route and check the correctness of + // transmission. + neo.fs.v2.session.ResponseVerificationHeader verify_header = 3; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/session/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/session/types.proto new file mode 100644 index 0000000..d1a9ef1 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/session/types.proto @@ -0,0 +1,238 @@ +syntax = "proto3"; + +package neo.fs.v2.session; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session/grpc;session"; +option csharp_namespace = "FrostFS.Session"; + +import "refs/types.proto"; +import "acl/types.proto"; +import "status/types.proto"; + +// Context information for Session Tokens related to ObjectService requests +message ObjectSessionContext { + // Object request verbs + enum Verb { + // Unknown verb + VERB_UNSPECIFIED = 0; + + // Refers to object.Put RPC call + PUT = 1; + + // Refers to object.Get RPC call + GET = 2; + + // Refers to object.Head RPC call + HEAD = 3; + + // Refers to object.Search RPC call + SEARCH = 4; + + // Refers to object.Delete RPC call + DELETE = 5; + + // Refers to object.GetRange RPC call + RANGE = 6; + + // Refers to object.GetRangeHash RPC call + RANGEHASH = 7; + } + // Type of request for which the token is issued + Verb verb = 1 [ json_name = "verb" ]; + + // Carries objects involved in the object session. + message Target { + // Indicates which container the session is spread to. Field MUST be set + // and correct. + refs.ContainerID container = 1 [ json_name = "container" ]; + + // Indicates which objects the session is spread to. Objects are expected + // to be stored in the NeoFS container referenced by `container` field. + // Each element MUST have correct format. + repeated refs.ObjectID objects = 2 [ json_name = "objects" ]; + } + // Object session target. MUST be correctly formed and set. If `objects` + // field is not empty, then the session applies only to these elements, + // otherwise, to all objects from the specified container. + Target target = 2 [ json_name = "target" ]; +} + +// Context information for Session Tokens related to ContainerService requests. +message ContainerSessionContext { + // Container request verbs + enum Verb { + // Unknown verb + VERB_UNSPECIFIED = 0; + + // Refers to container.Put RPC call + PUT = 1; + + // Refers to container.Delete RPC call + DELETE = 2; + + // Refers to container.SetExtendedACL RPC call + SETEACL = 3; + } + // Type of request for which the token is issued + Verb verb = 1 [ json_name = "verb" ]; + + // Spreads the action to all owner containers. + // If set, container_id field is ignored. + bool wildcard = 2 [ json_name = "wildcard" ]; + + // Particular container to which the action applies. + // Ignored if wildcard flag is set. + refs.ContainerID container_id = 3 [ json_name = "containerID" ]; +} + +// NeoFS Session Token. +message SessionToken { + // Session Token body + message Body { + // Token identifier is a valid UUIDv4 in binary form + bytes id = 1 [ json_name = "id" ]; + + // Identifier of the session initiator + neo.fs.v2.refs.OwnerID owner_id = 2 [ json_name = "ownerID" ]; + + // Lifetime parameters of the token. Field names taken from rfc7519. + message TokenLifetime { + // Expiration Epoch + uint64 exp = 1 [ json_name = "exp" ]; + + // Not valid before Epoch + uint64 nbf = 2 [ json_name = "nbf" ]; + + // Issued at Epoch + uint64 iat = 3 [ json_name = "iat" ]; + } + // Lifetime of the session + TokenLifetime lifetime = 3 [ json_name = "lifetime" ]; + + // Public key used in session + bytes session_key = 4 [ json_name = "sessionKey" ]; + + // Session Context information + oneof context { + // ObjectService session context + ObjectSessionContext object = 5 [ json_name = "object" ]; + + // ContainerService session context + ContainerSessionContext container = 6 [ json_name = "container" ]; + } + } + // Session Token contains the proof of trust between peers to be attached in + // requests for further verification. Please see corresponding section of + // NeoFS Technical Specification for details. + Body body = 1 [ json_name = "body" ]; + + // Signature of `SessionToken` information + neo.fs.v2.refs.Signature signature = 2 [ json_name = "signature" ]; +} + +// Extended headers for Request/Response. They may contain any user-defined +// headers to be interpreted on application level. +// +// Key name must be a unique valid UTF-8 string. Value can't be empty. Requests +// or Responses with duplicated header names or headers with empty values will +// be considered invalid. +// +// There are some "well-known" headers starting with `__SYSTEM__` (`__NEOFS__` +// is deprecated) prefix that affect system behaviour: +// +// * [ __SYSTEM__NETMAP_EPOCH ] \ +// (`__NEOFS__NETMAP_EPOCH` is deprecated) \ +// Netmap epoch to use for object placement calculation. The `value` is string +// encoded `uint64` in decimal presentation. If set to '0' or not set, the +// current epoch only will be used. +// * [ __SYSTEM__NETMAP_LOOKUP_DEPTH ] \ +// (`__NEOFS__NETMAP_LOOKUP_DEPTH` is deprecated) \ +// If object can't be found using current epoch's netmap, this header limits +// how many past epochs the node can look up through. The `value` is string +// encoded `uint64` in decimal presentation. If set to '0' or not set, only +// the current epoch will be used. +message XHeader { + // Key of the X-Header + string key = 1 [ json_name = "key" ]; + + // Value of the X-Header + string value = 2 [ json_name = "value" ]; +} + +// Meta information attached to the request. When forwarded between peers, +// request meta headers are folded in matryoshka style. +message RequestMetaHeader { + // Peer's API version used + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Peer's local epoch number. Set to 0 if unknown. + uint64 epoch = 2 [ json_name = "epoch" ]; + + // Maximum number of intermediate nodes in the request route + uint32 ttl = 3 [ json_name = "ttl" ]; + + // Request X-Headers + repeated XHeader x_headers = 4 [ json_name = "xHeaders" ]; + + // Session token within which the request is sent + SessionToken session_token = 5 [ json_name = "sessionToken" ]; + + // `BearerToken` with eACL overrides for the request + neo.fs.v2.acl.BearerToken bearer_token = 6 [ json_name = "bearerToken" ]; + + // `RequestMetaHeader` of the origin request + RequestMetaHeader origin = 7 [ json_name = "origin" ]; + + // NeoFS network magic. Must match the value for the network + // that the server belongs to. + uint64 magic_number = 8 [ json_name = "magicNumber" ]; +} + +// Information about the response +message ResponseMetaHeader { + // Peer's API version used + neo.fs.v2.refs.Version version = 1 [ json_name = "version" ]; + + // Peer's local epoch number + uint64 epoch = 2 [ json_name = "epoch" ]; + + // Maximum number of intermediate nodes in the request route + uint32 ttl = 3 [ json_name = "ttl" ]; + + // Response X-Headers + repeated XHeader x_headers = 4 [ json_name = "xHeaders" ]; + + // `ResponseMetaHeader` of the origin request + ResponseMetaHeader origin = 5 [ json_name = "origin" ]; + + // Status return + neo.fs.v2.status.Status status = 6 [ json_name = "status" ]; +} + +// Verification info for the request signed by all intermediate nodes. +message RequestVerificationHeader { + // Request Body signature. Should be generated once by the request initiator. + neo.fs.v2.refs.Signature body_signature = 1 [ json_name = "bodySignature" ]; + // Request Meta signature is added and signed by each intermediate node + neo.fs.v2.refs.Signature meta_signature = 2 [ json_name = "metaSignature" ]; + // Signature of previous hops + neo.fs.v2.refs.Signature origin_signature = 3 + [ json_name = "originSignature" ]; + + // Chain of previous hops signatures + RequestVerificationHeader origin = 4 [ json_name = "origin" ]; +} + +// Verification info for the response signed by all intermediate nodes +message ResponseVerificationHeader { + // Response Body signature. Should be generated once by an answering node. + neo.fs.v2.refs.Signature body_signature = 1 [ json_name = "bodySignature" ]; + // Response Meta signature is added and signed by each intermediate node + neo.fs.v2.refs.Signature meta_signature = 2 [ json_name = "metaSignature" ]; + // Signature of previous hops + neo.fs.v2.refs.Signature origin_signature = 3 + [ json_name = "originSignature" ]; + + // Chain of previous hops signatures + ResponseVerificationHeader origin = 4 [ json_name = "origin" ]; +} diff --git a/sdk/src/FrostFS.SDK.ProtosV2/status/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/status/types.proto new file mode 100644 index 0000000..8ab2f40 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/status/types.proto @@ -0,0 +1,157 @@ +syntax = "proto3"; + +package neo.fs.v2.status; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/status/grpc;status"; +option csharp_namespace = "FrostFS.Status"; + +// Declares the general format of the status returns of the NeoFS RPC protocol. +// Status is present in all response messages. Each RPC of NeoFS protocol +// describes the possible outcomes and details of the operation. +// +// Each status is assigned a one-to-one numeric code. Any unique result of an +// operation in NeoFS is unambiguously associated with the code value. +// +// Numerical set of codes is split into 1024-element sections. An enumeration +// is defined for each section. Values can be referred to in the following ways: +// +// * numerical value ranging from 0 to 4,294,967,295 (global code); +// +// * values from enumeration (local code). The formula for the ratio of the +// local code (`L`) of a defined section (`S`) to the global one (`G`): +// `G = 1024 * S + L`. +// +// All outcomes are divided into successful and failed, which corresponds +// to the success or failure of the operation. The definition of success +// follows the semantics of RPC and the description of its purpose. +// The server must not attach code that is the opposite of the outcome type. +// +// See the set of return codes in the description for calls. +// +// Each status can carry a developer-facing error message. It should be a human +// readable text in English. The server should not transmit (and the client +// should not expect) useful information in the message. Field `details` +// should make the return more detailed. +message Status { + // The status code + uint32 code = 1; + + // Developer-facing error message + string message = 2; + + // Return detail. It contains additional information that can be used to + // analyze the response. Each code defines a set of details that can be + // attached to a status. Client should not handle details that are not + // covered by the code. + message Detail { + // Detail ID. The identifier is required to determine the binary format + // of the detail and how to decode it. + uint32 id = 1; + + // Binary status detail. Must follow the format associated with ID. + // The possibility of missing a value must be explicitly allowed. + bytes value = 2; + } + + // Data detailing the outcome of the operation. Must be unique by ID. + repeated Detail details = 3; +} + +// Section identifiers. +enum Section { + // Successful return codes. + SECTION_SUCCESS = 0; + + // Failure codes regardless of the operation. + SECTION_FAILURE_COMMON = 1; + + // Object service-specific errors. + SECTION_OBJECT = 2; + + // Container service-specific errors. + SECTION_CONTAINER = 3; + + // Session service-specific errors. + SECTION_SESSION = 4; + + // Session service-specific errors. + SECTION_APE_MANAGER = 5; +} + +// Section of NeoFS successful return codes. +enum Success { + // [**0**] Default success. Not detailed. + // If the server cannot match successful outcome to the code, it should + // use this code. + OK = 0; +} + +// Section of failed statuses independent of the operation. +enum CommonFail { + // [**1024**] Internal server error, default failure. Not detailed. + // If the server cannot match failed outcome to the code, it should + // use this code. + INTERNAL = 0; + + // [**1025**] Wrong magic of the NeoFS network. + // Details: + // - [**0**] Magic number of the served NeoFS network (big-endian 64-bit + // unsigned integer). + WRONG_MAGIC_NUMBER = 1; + + // [**1026**] Signature verification failure. + SIGNATURE_VERIFICATION_FAIL = 2; + + // [**1027**] Node is under maintenance. + NODE_UNDER_MAINTENANCE = 3; +} + +// Section of statuses for object-related operations. +enum Object { + // [**2048**] Access denied by ACL. + // Details: + // - [**0**] Human-readable description (UTF-8 encoded string). + ACCESS_DENIED = 0; + + // [**2049**] Object not found. + OBJECT_NOT_FOUND = 1; + + // [**2050**] Operation rejected by the object lock. + LOCKED = 2; + + // [**2051**] Locking an object with a non-REGULAR type rejected. + LOCK_NON_REGULAR_OBJECT = 3; + + // [**2052**] Object has been marked deleted. + OBJECT_ALREADY_REMOVED = 4; + + // [**2053**] Invalid range has been requested for an object. + OUT_OF_RANGE = 5; +} + +// Section of statuses for container-related operations. +enum Container { + // [**3072**] Container not found. + CONTAINER_NOT_FOUND = 0; + + // [**3073**] eACL table not found. + EACL_NOT_FOUND = 1; + + // [**3074**] Container access denied. + CONTAINER_ACCESS_DENIED = 2; +} + +// Section of statuses for session-related operations. +enum Session { + // [**4096**] Token not found. + TOKEN_NOT_FOUND = 0; + + // [**4097**] Token has expired. + TOKEN_EXPIRED = 1; +} + +// Section of status for APE manager related operations. +enum APEManager { + // [**5120**] The operation is denied by APE manager. + APE_MANAGER_ACCESS_DENIED = 0; +} \ No newline at end of file diff --git a/sdk/src/FrostFS.SDK.ProtosV2/tombstone/types.proto b/sdk/src/FrostFS.SDK.ProtosV2/tombstone/types.proto new file mode 100644 index 0000000..739bef4 --- /dev/null +++ b/sdk/src/FrostFS.SDK.ProtosV2/tombstone/types.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package neo.fs.v2.tombstone; + +option go_package = "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/tombstone/grpc;tombstone"; +option csharp_namespace = "FrostFS.Tombstone"; + +import "refs/types.proto"; + +// Tombstone keeps record of deleted objects for a few epochs until they are +// purged from the NeoFS network. +message Tombstone { + // Last NeoFS epoch number of the tombstone lifetime. It's set by the + // tombstone creator depending on the current NeoFS network settings. A + // tombstone object must have the same expiration epoch value in + // `__SYSTEM__EXPIRATION_EPOCH` (`__NEOFS__EXPIRATION_EPOCH` is deprecated) + // attribute. Otherwise, the tombstone will be rejected by a storage node. + uint64 expiration_epoch = 1 [ json_name = "expirationEpoch" ]; + + // 16 byte UUID used to identify the split object hierarchy parts. Must be + // unique inside a container. All objects participating in the split must + // have the same `split_id` value. + bytes split_id = 2 [ json_name = "splitID" ]; + + // List of objects to be deleted. + repeated neo.fs.v2.refs.ObjectID members = 3 [ json_name = "members" ]; +}