From c9058fbb5a55640f18d68849c4708b59751f53c9 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Fri, 13 Oct 2023 12:36:49 +0200 Subject: [PATCH 01/21] Added type for `PendingUpdate` --- .../GetBlockPendingUpdates/GetBlocks.csproj | 18 +++++++ examples/GetBlockPendingUpdates/Program.cs | 49 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 examples/GetBlockPendingUpdates/GetBlocks.csproj create mode 100644 examples/GetBlockPendingUpdates/Program.cs diff --git a/examples/GetBlockPendingUpdates/GetBlocks.csproj b/examples/GetBlockPendingUpdates/GetBlocks.csproj new file mode 100644 index 00000000..f9d1b111 --- /dev/null +++ b/examples/GetBlockPendingUpdates/GetBlocks.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/GetBlockPendingUpdates/Program.cs b/examples/GetBlockPendingUpdates/Program.cs new file mode 100644 index 00000000..bb80c084 --- /dev/null +++ b/examples/GetBlockPendingUpdates/Program.cs @@ -0,0 +1,49 @@ +using CommandLine; +using Concordium.Sdk.Client; +using Concordium.Sdk.Types; + +// We disable these warnings since CommandLine needs to set properties in options +// but we don't want to give default values. +#pragma warning disable CS8618 + +namespace GetBlockPendingUpdates; + +internal sealed class GetBlockPendingUpdatesOptions +{ + [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", + Default = "http://node.testnet.concordium.com:20000/")] + public string Endpoint { get; set; } + [Option( + 'b', + "block-hash", + HelpText = "Block hash of the block." + )] + public string BlockHash { get; set; } +} + +public static class Program +{ + /// + /// Example how to use + /// + public static async Task Main(string[] args) => + await Parser.Default + .ParseArguments(args) + .WithParsedAsync(Run); + + private static async Task Run(GetBlockPendingUpdatesOptions o) + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + using var client = new ConcordiumClient(new Uri(o.Endpoint), new ConcordiumClientOptions()); + + IBlockHashInput bi = o.BlockHash != null ? new Given(BlockHash.From(o.BlockHash)) : new LastFinal(); + + var blocks = await client.GetBlockPendingUpdates(bi); + + await foreach (var block in blocks.Response) + { + Console.WriteLine($"Block arrived: {block}"); + } + } +} From 3c9e39a3a11a22ef4dd13ce1a3f76be91a534c1c Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 26 Oct 2023 09:00:10 +0200 Subject: [PATCH 02/21] Working on adding account transactions --- src/Transactions/AccountSignatureMap.cs | 9 ++++ src/Transactions/AccountTransactionHeader.cs | 13 ++++++ .../AccountTransactionSignature.cs | 12 +++++ src/Types/BlockItem.cs | 45 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 src/Types/BlockItem.cs diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index e0015fa5..f0681c55 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -56,4 +56,13 @@ public Grpc.V2.AccountSignatureMap ToProto() } return accountSignatureMap; } + + internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) { + var dict = new Dictionary(); + foreach (var s in map.Signatures) + { + dict.Add(new AccountKeyIndex((byte) s.Key), s.Value.Value.ToByteArray()); + } + return AccountSignatureMap.Create(dict); + } } diff --git a/src/Transactions/AccountTransactionHeader.cs b/src/Transactions/AccountTransactionHeader.cs index 92db91f6..6d6b918c 100644 --- a/src/Transactions/AccountTransactionHeader.cs +++ b/src/Transactions/AccountTransactionHeader.cs @@ -87,4 +87,17 @@ public Grpc.V2.AccountTransactionHeader ToProto() => Expiry = this.Expiry.ToProto(), EnergyAmount = new Grpc.V2.Energy() { Value = this.MaxEnergyCost.Value } }; + + /// + /// Converts the account transaction header to its corresponding protocol buffer message instance. + /// + internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader) { + return new AccountTransactionHeader( + AccountAddress.From(accountTransactionHeader.Sender), + AccountSequenceNumber.From(accountTransactionHeader.SequenceNumber), + Expiry.From(accountTransactionHeader.Expiry.Value), + EnergyAmount.From(accountTransactionHeader.EnergyAmount), + new PayloadSize((uint) accountTransactionHeader.CalculateSize()) + ); + } } diff --git a/src/Transactions/AccountTransactionSignature.cs b/src/Transactions/AccountTransactionSignature.cs index 736100af..dc1b1745 100644 --- a/src/Transactions/AccountTransactionSignature.cs +++ b/src/Transactions/AccountTransactionSignature.cs @@ -72,4 +72,16 @@ public Grpc.V2.AccountTransactionSignature ToProto() .ForEach(x => accountTransactionSignature.Signatures.Add(x.Key.Value, x.Value.ToProto())); return accountTransactionSignature; } + + internal static AccountTransactionSignature From(Grpc.V2.AccountTransactionSignature signature) + { + var dict = new Dictionary(); + foreach (var s in signature.Signatures) { + dict.Add( + new AccountCredentialIndex((byte) s.Key), + AccountSignatureMap.From(s.Value) + ); + } + return new AccountTransactionSignature(dict); + } } diff --git a/src/Types/BlockItem.cs b/src/Types/BlockItem.cs new file mode 100644 index 00000000..ab81548d --- /dev/null +++ b/src/Types/BlockItem.cs @@ -0,0 +1,45 @@ +//using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Transactions; +//using GrpcEffect = Concordium.Grpc.V2.PendingUpdate.EffectOneofCase; + +namespace Concordium.Sdk.Types; +/// +/// Minimum stake needed to become a baker. This only applies to protocol version 1-3. +/// +/// Minimum threshold required for registering as a baker. +public record BakerStakeThreshold(CcdAmount MinimumThresholdForBaking) +{ + internal static BakerStakeThreshold From(Grpc.V2.BakerStakeThreshold bakerStakeThreshold) => new(CcdAmount.From(bakerStakeThreshold.BakerStakeThreshold_)); +}; + +/// +/// A pending update. +/// +/// The effective time of the update. +/// The effect of the update. +//public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType]) +//{ +// internal static BlockItem From(Grpc.V2.BlockItem blockItem) => { +// throw new NotImplementedException(); +// } +//} + +/// The effect of the update. +public abstract record BlockItemType; + +/// Updates to the root keys. +public sealed record AccountTransaction( + AccountTransactionSignature accountTransactionSignature, + AccountTransactionHeader accountTransactionHeader, + AccountTransactionPayload accountTransactionPayload +) : BlockItemType { + internal static AccountTransaction From(Grpc.V2.AccountTransaction accountTransaction) { + return new AccountTransaction( + AccountTransactionSignature.From(accountTransaction.Signature), + AccountTransactionHeader.From(accountTransaction.Header), + AccountTransactionPayload.From(accountTransaction.Payload) + ); + } +} + +//public sealed record AccountTransactionSignature From b7d47a5c996a78eee732107af76ea1d635016c74 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 26 Oct 2023 11:24:40 +0200 Subject: [PATCH 03/21] Worked on adding credential deployment --- src/Types/BlockItem.cs | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/Types/BlockItem.cs b/src/Types/BlockItem.cs index ab81548d..33a778a8 100644 --- a/src/Types/BlockItem.cs +++ b/src/Types/BlockItem.cs @@ -1,28 +1,41 @@ -//using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Transactions; -//using GrpcEffect = Concordium.Grpc.V2.PendingUpdate.EffectOneofCase; +using GrpcPayload = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; namespace Concordium.Sdk.Types; + +public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload) { + internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => + new CredentialDeployment( + TransactionTime.From(cred.MessageExpiry), + cred.PayloadCase switch { + GrpcPayload.RawPayload => CredentialRawPayload.From(cred.RawPayload) + GrpcPayload.None => throw new NotImplementedException(), + _ => throw new MissingEnumException(cred.PayloadCase), + } + + ); +} + +/// The payload of a Credential Deployment. +public interface ICredentialPayload{}; + /// -/// Minimum stake needed to become a baker. This only applies to protocol version 1-3. +/// A raw payload, which is just the encoded payload. +/// A typed variant might be added in the future. /// -/// Minimum threshold required for registering as a baker. -public record BakerStakeThreshold(CcdAmount MinimumThresholdForBaking) -{ - internal static BakerStakeThreshold From(Grpc.V2.BakerStakeThreshold bakerStakeThreshold) => new(CcdAmount.From(bakerStakeThreshold.BakerStakeThreshold_)); -}; +public sealed record CredentialRawPayload(byte[] RawPayload) : ICredentialPayload; /// /// A pending update. /// /// The effective time of the update. /// The effect of the update. -//public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType]) -//{ -// internal static BlockItem From(Grpc.V2.BlockItem blockItem) => { -// throw new NotImplementedException(); -// } -//} +public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType) +{ + internal static BlockItem From(Grpc.V2.BlockItem blockItem) => + throw new NotImplementedException(); +} /// The effect of the update. public abstract record BlockItemType; From cf13b8963cbb01384ef80e84c446805ac2430de6 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Thu, 2 Nov 2023 16:07:41 +0100 Subject: [PATCH 04/21] Did more stuff --- src/Transactions/AccountSignatureMap.cs | 52 ++++++++ src/Transactions/AccountTransactionPayload.cs | 18 +++ src/Transactions/DeployModule.cs | 34 +++++ src/Transactions/SignedAccountTransaction.cs | 15 ++- src/Types/BlockItem.cs | 118 +++++++++++++----- src/Types/VersionedModuleSource.cs | 4 + 6 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 src/Transactions/DeployModule.cs diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index f0681c55..abb1211f 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -66,3 +66,55 @@ internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) { return AccountSignatureMap.Create(dict); } } + + +/// Index of a key in an authorizations update payload. +/// The key index represented by a . +public sealed record UpdateKeysIndex(byte Value); + +/// A map from to signatures. +public sealed record UpdateInstructionSignatureMap +{ + /// Internal representation of the map. + public ImmutableDictionary Signatures { get; init; } + + /// Initializes a new instance of the class. + /// A map from account key indices to signatures. + private UpdateInstructionSignatureMap(Dictionary signatures) => this.Signatures = signatures.ToImmutableDictionary(); + + /// Creates a new instance of the class. + /// A map from account key indices to signatures. + /// A signature is not 64 bytes. + public static UpdateInstructionSignatureMap Create(Dictionary signatures) + { + // Signatures are 64-byte ed25519 signatures and therefore 64 bytes. + if (signatures.Values.Any(signature => signature.Length != 64)) + { + throw new ArgumentException($"Signature should be {64} bytes."); + } + return new UpdateInstructionSignatureMap(signatures); + } + + /// Converts the account signature map to its corresponding protocol buffer message instance. + public Grpc.V2.SignatureMap ToProto() + { + var signatureMap = new Grpc.V2.SignatureMap(); + foreach (var s in this.Signatures) + { + signatureMap.Signatures.Add( + s.Key.Value, + new Grpc.V2.Signature() { Value = Google.Protobuf.ByteString.CopyFrom(s.Value) } + ); + } + return signatureMap; + } + + internal static UpdateInstructionSignatureMap From(Grpc.V2.SignatureMap map) { + var dict = new Dictionary(); + foreach (var s in map.Signatures) + { + dict.Add(new UpdateKeysIndex((byte) s.Key), s.Value.Value.ToByteArray()); + } + return UpdateInstructionSignatureMap.Create(dict); + } +} diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 7f84e2bf..e314af9f 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -1,4 +1,6 @@ using Concordium.Sdk.Types; +using Concordium.Sdk.Exceptions; +using PayloadCase = Concordium.Grpc.V2.AccountTransactionPayload.PayloadOneofCase; namespace Concordium.Sdk.Transactions; @@ -47,4 +49,20 @@ Expiry expiry /// public Grpc.V2.AccountTransactionPayload ToProto() => new() { RawPayload = Google.Protobuf.ByteString.CopyFrom(this.ToBytes()) }; + + internal static AccountTransactionPayload From(Grpc.V2.AccountTransactionPayload payload) { + return payload.PayloadCase switch { + PayloadCase.TransferWithMemo => new TransferWithMemo( + CcdAmount.From(payload.TransferWithMemo.Amount), + AccountAddress.From(payload.TransferWithMemo.Receiver), + // Following line complains that 'Memo' might be null but accompanying comment states explicitly that it can't be. + OnChainData.From(payload.TransferWithMemo.Memo) + ), + PayloadCase.Transfer => new Transfer( + CcdAmount.From(payload.Transfer.Amount), + AccountAddress.From(payload.Transfer.Receiver) + ), + _ => throw new MissingEnumException(payload.PayloadCase), + }; + } } diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs new file mode 100644 index 00000000..3b66c79e --- /dev/null +++ b/src/Transactions/DeployModule.cs @@ -0,0 +1,34 @@ + +using Concordium.Sdk.Types; + +namespace Concordium.Sdk.Transactions; + +/// A deployment of a Wasm smart contract module. +/// The smart contract module to be deployed. +public sealed record DeployModule(VersionedModuleSource Module) : AccountTransactionPayload +{ + /// The account transaction type to be used in the serialized payload. + private const byte TransactionType = (byte)Types.TransactionType.DeployModule; + + /// + /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. + /// + /// The smart contract module to be deployed. + private static byte[] Serialize(VersionedModuleSource module) + { + using var memoryStream = new MemoryStream((int)( + sizeof(TransactionType) + + AccountAddress.BytesLength + + module.Source.Length + + CcdAmount.BytesLength)); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(receiver.ToBytes()); + memoryStream.Write(memo.ToBytes()); + memoryStream.Write(amount.ToBytes()); + return memoryStream.ToArray(); + } + + public override ulong GetTransactionSpecificCost() => 300; + + public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver, this.Memo); +} diff --git a/src/Transactions/SignedAccountTransaction.cs b/src/Transactions/SignedAccountTransaction.cs index f35aa64a..c2bd5b07 100644 --- a/src/Transactions/SignedAccountTransaction.cs +++ b/src/Transactions/SignedAccountTransaction.cs @@ -1,4 +1,5 @@ using Concordium.Grpc.V2; +using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -19,10 +20,10 @@ public record SignedAccountTransaction( AccountTransactionHeader Header, AccountTransactionPayload Payload, AccountTransactionSignature Signature - ) - +): BlockItemType { - public AccountTransaction ToProto() => + /// Converts this type to the equivalent protocol buffer type. + public Grpc.V2.AccountTransaction ToProto() => new() { Header = this.Header.ToProto(), @@ -30,6 +31,14 @@ public AccountTransaction ToProto() => Signature = this.Signature.ToProto(), }; + internal static SignedAccountTransaction From(Grpc.V2.AccountTransaction accountTransaction) { + return new SignedAccountTransaction( + AccountTransactionHeader.From(accountTransaction.Header), + AccountTransactionPayload.From(accountTransaction.Payload), + AccountTransactionSignature.From(accountTransaction.Signature) + ); + } + /// /// Converts the signed account transaction to a protocol buffer /// message instance which is compatible with diff --git a/src/Types/BlockItem.cs b/src/Types/BlockItem.cs index 33a778a8..3e3fd7a3 100644 --- a/src/Types/BlockItem.cs +++ b/src/Types/BlockItem.cs @@ -1,58 +1,112 @@ using Concordium.Sdk.Exceptions; using Concordium.Sdk.Transactions; -using GrpcPayload = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; +using CredentialDeploymentPayloadCase = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; +using UpdateInstructionPayloadCase = Concordium.Grpc.V2.UpdateInstructionPayload.PayloadOneofCase; +using BlockItemCase = Concordium.Grpc.V2.BlockItem.BlockItemOneofCase; namespace Concordium.Sdk.Types; -public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload) { +/// +/// Update instructions are messages which can update the chain parameters. Including which keys are allowed +/// to make future update instructions. +/// +/// A map from `UpdateKeysIndex` to `Signature`. Keys must not exceed 2^16. +/// The header of the UpdateInstruction. +/// The payload of the UpdateInstruction. Can currently only be a `RawPayload` +public record UpdateInstruction( + SignatureMap SignatureMap, + UpdateInstructionHeader Header, + IUpdateInstructionPayload Payload +): BlockItemType { + internal static UpdateInstruction From(Grpc.V2.UpdateInstruction updateInstruction) => + new UpdateInstruction( + SignatureMap.From(updateInstruction.Signatures), + UpdateInstructionHeader.From(updateInstruction.Header), + updateInstruction.Payload.PayloadCase switch { + UpdateInstructionPayloadCase.RawPayload => new UpdateInstructionPayloadRaw(updateInstruction.Payload.RawPayload.ToByteArray()), + UpdateInstructionPayloadCase.None => throw new NotImplementedException(), + _ => throw new MissingEnumException(updateInstruction.Payload.PayloadCase), + } + ); +} + +/// The header of an UpdateInstruction. +/// A sequence number that determines the ordering of update transactions. +/// When the update takes effect. +/// Latest time the update instruction can included in a block. +public record UpdateInstructionHeader( + UpdateSequenceNumber SequenceNumber, + TransactionTime EffectiveTime, + TransactionTime Timeout +) { + internal static UpdateInstructionHeader From(Grpc.V2.UpdateInstructionHeader header) => + new UpdateInstructionHeader( + UpdateSequenceNumber.From(header.SequenceNumber), + TransactionTime.From(header.EffectiveTime), + TransactionTime.From(header.Timeout) + ); +} + +/// +/// A sequence number that determines the ordering of update transactions. +/// Equivalent to `SequenceNumber` for account transactions. +/// Update sequence numbers are per update type and the minimum value is 1. +/// +public record UpdateSequenceNumber(UInt64 SequenceNumber) { + internal static UpdateSequenceNumber From(Grpc.V2.UpdateSequenceNumber sequenceNumber) => + new UpdateSequenceNumber(sequenceNumber.Value); +} + +/// The payload for an UpdateInstruction. +public interface IUpdateInstructionPayload{} + +/// A raw payload encoded according to the format defined by the protocol. +public sealed record UpdateInstructionPayloadRaw(byte[] RawPayload) : IUpdateInstructionPayload; + +/// +/// Credential deployments create new accounts. They are not paid for +/// directly by the sender. Instead, bakers are rewarded by the protocol for +/// including them. +/// +/// Latest time the credential deployment can included in a block. +/// The payload of the credential deployment. +public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload): BlockItemType { internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => new CredentialDeployment( TransactionTime.From(cred.MessageExpiry), cred.PayloadCase switch { - GrpcPayload.RawPayload => CredentialRawPayload.From(cred.RawPayload) - GrpcPayload.None => throw new NotImplementedException(), - _ => throw new MissingEnumException(cred.PayloadCase), + CredentialDeploymentPayloadCase.RawPayload => new CredentialPayloadRaw(cred.RawPayload.ToByteArray()), + CredentialDeploymentPayloadCase.None => throw new NotImplementedException(), + _ => throw new MissingEnumException(cred.PayloadCase), } - ); } -/// The payload of a Credential Deployment. +/// The payload of a Credential Deployment. public interface ICredentialPayload{}; /// /// A raw payload, which is just the encoded payload. /// A typed variant might be added in the future. /// -public sealed record CredentialRawPayload(byte[] RawPayload) : ICredentialPayload; +public sealed record CredentialPayloadRaw(byte[] RawPayload) : ICredentialPayload; -/// -/// A pending update. -/// -/// The effective time of the update. -/// The effect of the update. +/// A block item. +/// The hash of the block item that identifies it to the chain. +/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType) { internal static BlockItem From(Grpc.V2.BlockItem blockItem) => - throw new NotImplementedException(); -} - -/// The effect of the update. -public abstract record BlockItemType; - -/// Updates to the root keys. -public sealed record AccountTransaction( - AccountTransactionSignature accountTransactionSignature, - AccountTransactionHeader accountTransactionHeader, - AccountTransactionPayload accountTransactionPayload -) : BlockItemType { - internal static AccountTransaction From(Grpc.V2.AccountTransaction accountTransaction) { - return new AccountTransaction( - AccountTransactionSignature.From(accountTransaction.Signature), - AccountTransactionHeader.From(accountTransaction.Header), - AccountTransactionPayload.From(accountTransaction.Payload) + new BlockItem( + TransactionHash.From(blockItem.Hash.ToString()), + blockItem.BlockItemCase switch { + BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), + BlockItemCase.CredentialDeployment => CredentialDeployment.From(blockItem.CredentialDeployment), + BlockItemCase.UpdateInstruction => UpdateInstruction.From(blockItem.UpdateInstruction), + _ => throw new MissingEnumException(blockItem.BlockItemCase), + } ); - } } -//public sealed record AccountTransactionSignature +/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. +public abstract record BlockItemType; diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 35cd5ffd..57316480 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -29,6 +29,8 @@ public sealed record ModuleV0(byte[] Source) : VersionedModuleSource(Source) { internal static ModuleV0 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV0 moduleSourceV0) => new(moduleSourceV0.Value.ToByteArray()); + + public byte[] ToBytes() => this.Source.Prepend((byte) 0).ToArray(); } /// @@ -39,4 +41,6 @@ public sealed record ModuleV1(byte[] Source) : VersionedModuleSource(Source) { internal static ModuleV1 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV1 moduleSourceV1) => new(moduleSourceV1.Value.ToByteArray()); + + public byte[] ToBytes() => this.Source.Prepend((byte) 1).ToArray(); } From c34ce3bc4e2b529a29400af490221f9fb74d88f6 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 7 Nov 2023 16:07:59 +0100 Subject: [PATCH 05/21] Implemented serialization+deserialization for `deployModule` --- src/Helpers/Deserialization.cs | 47 +++++++ src/Transactions/AccountSignatureMap.cs | 7 +- src/Transactions/AccountTransactionPayload.cs | 7 +- src/Transactions/DeployModule.cs | 37 +++-- src/Types/BlockItem.cs | 4 +- src/Types/OnChainData.cs | 5 + src/Types/VersionedModuleSource.cs | 128 +++++++++++++++++- tests/UnitTests/Transactions/DeployModule.cs | 57 ++++++++ 8 files changed, 272 insertions(+), 20 deletions(-) create mode 100644 src/Helpers/Deserialization.cs create mode 100644 tests/UnitTests/Transactions/DeployModule.cs diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs new file mode 100644 index 00000000..fea758fa --- /dev/null +++ b/src/Helpers/Deserialization.cs @@ -0,0 +1,47 @@ +using System.Buffers.Binary; + +namespace Concordium.Sdk.Helpers; + +/// +/// Error on deserialization. +/// +public enum DeserialErr +{ + TooShort, + InvalidModuleVersion, +} + +/// +/// Helpers for deserializing data. +/// +public static class Deserial +{ + /// + /// Creates a uint from a byte array. + /// + public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, DeserialErr? Error) output) + { + var offset_input = input.Skip(offset).ToArray(); + + if (offset_input.Length < 4) { + output = (null, DeserialErr.TooShort); + return false; + } + + var bytes = offset_input.Take(4).ToArray(); + + output = (BinaryPrimitives.ReadUInt32BigEndian(bytes), null); + return true; + } + + private static void PrintBytes(String msg, byte[] bytes) { + Console.WriteLine(msg); + foreach (byte b in bytes) { + Console.Write(b); + Console.Write(" "); + } + Console.Write("\n"); + } +} + + diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index abb1211f..2b27200a 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -69,7 +69,6 @@ internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) { /// Index of a key in an authorizations update payload. -/// The key index represented by a . public sealed record UpdateKeysIndex(byte Value); /// A map from to signatures. @@ -79,11 +78,11 @@ public sealed record UpdateInstructionSignatureMap public ImmutableDictionary Signatures { get; init; } /// Initializes a new instance of the class. - /// A map from account key indices to signatures. + /// A map from update key indices to signatures. private UpdateInstructionSignatureMap(Dictionary signatures) => this.Signatures = signatures.ToImmutableDictionary(); /// Creates a new instance of the class. - /// A map from account key indices to signatures. + /// A map from update key indices to signatures. /// A signature is not 64 bytes. public static UpdateInstructionSignatureMap Create(Dictionary signatures) { @@ -95,7 +94,7 @@ public static UpdateInstructionSignatureMap Create(DictionaryConverts the account signature map to its corresponding protocol buffer message instance. + /// Converts the update signature map to its corresponding protocol buffer message instance. public Grpc.V2.SignatureMap ToProto() { var signatureMap = new Grpc.V2.SignatureMap(); diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index e314af9f..1de6518e 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -55,13 +55,18 @@ internal static AccountTransactionPayload From(Grpc.V2.AccountTransactionPayload PayloadCase.TransferWithMemo => new TransferWithMemo( CcdAmount.From(payload.TransferWithMemo.Amount), AccountAddress.From(payload.TransferWithMemo.Receiver), - // Following line complains that 'Memo' might be null but accompanying comment states explicitly that it can't be. OnChainData.From(payload.TransferWithMemo.Memo) ), PayloadCase.Transfer => new Transfer( CcdAmount.From(payload.Transfer.Amount), AccountAddress.From(payload.Transfer.Receiver) ), + PayloadCase.RegisterData => new RegisterData( + OnChainData.From(payload.RegisterData) + ), + PayloadCase.DeployModule => new DeployModule( + VersionedModuleSourceFactory.From(payload.DeployModule) + ), _ => throw new MissingEnumException(payload.PayloadCase), }; } diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 3b66c79e..19cc1cbe 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -1,4 +1,4 @@ - +using Concordium.Sdk.Helpers; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -11,24 +11,41 @@ public sealed record DeployModule(VersionedModuleSource Module) : AccountTransac private const byte TransactionType = (byte)Types.TransactionType.DeployModule; /// - /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. + /// Copies the "deploy module" account transaction in the binary format expected by the node to a byte array. /// - /// The smart contract module to be deployed. + /// The smart contract module to be deployed. private static byte[] Serialize(VersionedModuleSource module) { using var memoryStream = new MemoryStream((int)( sizeof(TransactionType) + - AccountAddress.BytesLength + - module.Source.Length + - CcdAmount.BytesLength)); + module.BytesLength + )); memoryStream.WriteByte(TransactionType); - memoryStream.Write(receiver.ToBytes()); - memoryStream.Write(memo.ToBytes()); - memoryStream.Write(amount.ToBytes()); + memoryStream.Write(module.ToBytes()); return memoryStream.ToArray(); } + + /// + /// Create a "deploy module" payload from a serialized as bytes. + /// + /// The "deploy module" payload as bytes. + public static bool TryDeserial(byte[] bytes, out (DeployModule? ContractName, DeserialErr? Error) output) { + (VersionedModuleSource?, DeserialErr?) module = (null, null); + + var deserialSuccess = VersionedModuleSourceFactory.TryDeserialize(bytes.Skip(1).ToArray(), out module); + + if (!deserialSuccess) { + output = (null, module.Item2); + return false; + }; + + output = (new DeployModule(module.Item1), null); + return false; + } + public override ulong GetTransactionSpecificCost() => 300; - public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver, this.Memo); + public override byte[] ToBytes() => Serialize(this.Module); } + diff --git a/src/Types/BlockItem.cs b/src/Types/BlockItem.cs index 3e3fd7a3..ddec6c2b 100644 --- a/src/Types/BlockItem.cs +++ b/src/Types/BlockItem.cs @@ -14,13 +14,13 @@ namespace Concordium.Sdk.Types; /// The header of the UpdateInstruction. /// The payload of the UpdateInstruction. Can currently only be a `RawPayload` public record UpdateInstruction( - SignatureMap SignatureMap, + UpdateInstructionSignatureMap SignatureMap, UpdateInstructionHeader Header, IUpdateInstructionPayload Payload ): BlockItemType { internal static UpdateInstruction From(Grpc.V2.UpdateInstruction updateInstruction) => new UpdateInstruction( - SignatureMap.From(updateInstruction.Signatures), + UpdateInstructionSignatureMap.From(updateInstruction.Signatures), UpdateInstructionHeader.From(updateInstruction.Header), updateInstruction.Payload.PayloadCase switch { UpdateInstructionPayloadCase.RawPayload => new UpdateInstructionPayloadRaw(updateInstruction.Payload.RawPayload.ToByteArray()), diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index d07e9394..63d9ff8b 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -145,4 +145,9 @@ public byte[] ToBytes() return From(memo.Value.ToByteArray()); } + + internal static OnChainData From(Grpc.V2.RegisteredData registeredData) + { + return From(registeredData.Value.ToByteArray()); + } } diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 57316480..132e967b 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -1,11 +1,26 @@ using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Helpers; namespace Concordium.Sdk.Types; /// /// Contains source code of a versioned module where inherited classes are concrete versions. /// -public abstract record VersionedModuleSource(byte[] Source); +public abstract record VersionedModuleSource(byte[] Source) { + internal const uint MaxLength = 8 * 65536; + internal uint BytesLength = 2 * sizeof(int) + (uint) Source.Length; + + internal abstract uint GetVersion(); + + internal byte[] ToBytes() { + using var memoryStream = new MemoryStream((int)(BytesLength)); + memoryStream.Write(Serialization.ToBytes(GetVersion())); + memoryStream.Write(Serialization.ToBytes((uint) Source.Length)); + memoryStream.Write(Source); + return memoryStream.ToArray(); + } + +} internal static class VersionedModuleSourceFactory { @@ -19,6 +34,33 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version _ => throw new MissingEnumException(versionedModuleSource .ModuleCase) }; + + internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? ContractName, DeserialErr? Error) output) { + (uint?, DeserialErr?) version = (null, null); + var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out version); + + if (!versionSuccess) { + output = (null, version.Item2); + return false; + } + if (bytes.Length < 8) { + output = (null, DeserialErr.TooShort); + return false; + } + + var rest = bytes.Skip(8).ToArray(); + + if (version.Item1 == 0) { + output = (ModuleV0.From(rest), null); + return true; + } else if (version.Item1== 1) { + output = (ModuleV0.From(rest), null); + return true; + } else { + output = (null, DeserialErr.InvalidModuleVersion); + return false; + }; + } } /// @@ -27,10 +69,45 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version /// Source code of module public sealed record ModuleV0(byte[] Source) : VersionedModuleSource(Source) { + override internal uint GetVersion() => 0; + internal static ModuleV0 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV0 moduleSourceV0) => new(moduleSourceV0.Value.ToByteArray()); - public byte[] ToBytes() => this.Source.Prepend((byte) 0).ToArray(); + /// + /// Creates a WASM-module from byte array. + /// + /// WASM-module as a byte array. + /// The length of the supplied module exceeds "MaxLength". + public static ModuleV0 From(byte[] source) + { + if (source.Length > MaxLength) + { + throw new ArgumentException( + $"Size of a data is not allowed to exceed {MaxLength} bytes." + ); + } + + return new ModuleV0(source.ToArray()); + } + + /// + /// Creates an instance from a hex encoded string. + /// + /// The WASM-module represented as a hex encoded string representing at most "MaxLength" bytes. + /// The supplied string is not a hex encoded WASM-module representing at most "MaxLength" bytes. + public static ModuleV0 FromHex(string hexString) + { + try + { + var value = Convert.FromHexString(hexString); + return From(value); + } + catch (Exception e) + { + throw new ArgumentException("The provided string is not hex encoded: ", e); + } + } } /// @@ -39,8 +116,53 @@ internal static ModuleV0 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV0 /// Source code of module public sealed record ModuleV1(byte[] Source) : VersionedModuleSource(Source) { + override internal uint GetVersion() => 1; + internal static ModuleV1 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV1 moduleSourceV1) => new(moduleSourceV1.Value.ToByteArray()); - public byte[] ToBytes() => this.Source.Prepend((byte) 1).ToArray(); + /// + /// Creates a WASM-module from byte array. + /// + /// WASM-module as a byte array. + /// The length of the supplied module exceeds "MaxLength". + public static ModuleV1 From(byte[] source) + { + if (source.Length > MaxLength) + { + throw new ArgumentException( + $"Size of a data is not allowed to exceed {MaxLength} bytes." + ); + } + + return new ModuleV1(source.ToArray()); + } + + /// + /// Creates an instance from a hex encoded string. + /// + /// The WASM-module represented as a hex encoded string representing at most "MaxLength" bytes. + /// The supplied string is not a hex encoded WASM-module representing at most "MaxLength" bytes. + public static ModuleV1 FromHex(string hexString) + { + try + { + var value = Convert.FromHexString(hexString); + return From(value); + } + catch (Exception e) + { + throw new ArgumentException("The provided string is not hex encoded: ", e); + } + } +} + +/// +/// Thrown when a matched enum value could not be handled in a switch statement. +/// +public sealed class InvalidModuleVersion : Exception +{ + internal InvalidModuleVersion(uint versionByte) : + base($"Unknown version byte: {versionByte}") + { } } diff --git a/tests/UnitTests/Transactions/DeployModule.cs b/tests/UnitTests/Transactions/DeployModule.cs new file mode 100644 index 00000000..f69d6ba1 --- /dev/null +++ b/tests/UnitTests/Transactions/DeployModule.cs @@ -0,0 +1,57 @@ +using System; +using Concordium.Sdk.Transactions; +using Concordium.Sdk.Helpers; +using Concordium.Sdk.Types; +using FluentAssertions; +using Xunit; + +namespace Concordium.Sdk.Tests.UnitTests.Transactions; + +public sealed class DeployModuleTests +{ + /// + /// Creates a new instance of the + /// transaction with a short WASM source + /// + public static DeployModule CreateDeployModule() + { + var source = new byte[] { + 0, 97, 115, 109, 1, 0, 0, 0, 4, 5, 1, 112, 1, 1, 1, 5, 3, 1, + 0, 16, 6, 25, 3, 127, 1, 65, 128, 128, 192, 0, 11, 127, 0, 65, + 128, 128, 192, 0, 11, 127, 0, 65, 128, 128, 192, 0, 11, 7, 37, + 3, 6, 109, 101, 109, 111, 114, 121, 2, 0, 10, 95, 95, 100, 97, + 116, 97, 95, 101, 110, 100, 3, 1, 11, 95, 95, 104, 101, 97, 112, + 95, 98, 97, 115, 101, 3, 2 + }; + var module = ModuleV1.From(source); + return new DeployModule(module); + } + + [Fact] + public void ToBytes_ReturnsCorrectValue() + { + // The expected payload was generated using the Concordium Rust SDK. + var expectedBytes = new byte[] { + 0, 0, 0, 0, 1, 0, 0, 0, 86, 0, 97, 115, 109, 1, 0, 0, 0, 4, 5, + 1, 112, 1, 1, 1, 5, 3, 1, 0, 16, 6, 25, 3, 127, 1, 65, 128, 128, + 192, 0, 11, 127, 0, 65, 128, 128, 192, 0, 11, 127, 0, 65, 128, + 128, 192, 0, 11, 7, 37, 3, 6, 109, 101, 109, 111, 114, 121, 2, + 0, 10, 95, 95, 100, 97, 116, 97, 95, 101, 110, 100, 3, 1, 11, + 95, 95, 104, 101, 97, 112, 95, 98, 97, 115, 101, 3, 2 + }; + + CreateDeployModule().ToBytes().Should().BeEquivalentTo(expectedBytes); + } + + [Fact] + public void ToBytes_InverseOfFromBytes() + { + // The expected payload was generated using the Concordium Rust SDK. + var moduleBytes = CreateDeployModule().ToBytes(); + + (DeployModule?, DeserialErr?) module = (null, null); + var deserialSuccess = DeployModule.TryDeserial(moduleBytes, out module); + + CreateDeployModule().Should().BeEquivalentTo(module.Item1); + } +} From 44b584179df01a385eed0508928d83cbf7329a6f Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 14 Nov 2023 13:55:53 +0100 Subject: [PATCH 06/21] Fixed DeployModule equality and cleanup --- src/Helpers/Deserialization.cs | 3 ++ src/Transactions/DeployModule.cs | 8 ++++-- src/Types/VersionedModuleSource.cs | 30 ++++++++++++++++---- tests/UnitTests/Transactions/DeployModule.cs | 30 +++++++++++++++++--- 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs index fea758fa..acf12195 100644 --- a/src/Helpers/Deserialization.cs +++ b/src/Helpers/Deserialization.cs @@ -9,6 +9,8 @@ public enum DeserialErr { TooShort, InvalidModuleVersion, + InvalidTransactionType, + InternalError, } /// @@ -34,6 +36,7 @@ public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, Des return true; } + // TODO: Debug tool remove private static void PrintBytes(String msg, byte[] bytes) { Console.WriteLine(msg); foreach (byte b in bytes) { diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 19cc1cbe..5c80a324 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -30,15 +30,17 @@ private static byte[] Serialize(VersionedModuleSource module) /// Create a "deploy module" payload from a serialized as bytes. /// /// The "deploy module" payload as bytes. + /// Where to write the result of the operation. public static bool TryDeserial(byte[] bytes, out (DeployModule? ContractName, DeserialErr? Error) output) { - (VersionedModuleSource?, DeserialErr?) module = (null, null); - - var deserialSuccess = VersionedModuleSourceFactory.TryDeserialize(bytes.Skip(1).ToArray(), out module); + var deserialSuccess = VersionedModuleSourceFactory.TryDeserialize(bytes.Skip(1).ToArray(), out var module); if (!deserialSuccess) { output = (null, module.Item2); return false; }; + if (bytes[0] != TransactionType) { + output = (null, DeserialErr.InvalidTransactionType); + } output = (new DeployModule(module.Item1), null); return false; diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 132e967b..f69fa194 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -6,7 +6,7 @@ namespace Concordium.Sdk.Types; /// /// Contains source code of a versioned module where inherited classes are concrete versions. /// -public abstract record VersionedModuleSource(byte[] Source) { +public abstract record VersionedModuleSource(byte[] Source) : IEquatable { internal const uint MaxLength = 8 * 65536; internal uint BytesLength = 2 * sizeof(int) + (uint) Source.Length; @@ -20,6 +20,27 @@ internal byte[] ToBytes() { return memoryStream.ToArray(); } + //public override bool Equals(object obj) + //{ + // return Equals(obj as VersionedModuleSource); + //} + + public virtual bool Equals(VersionedModuleSource? other) + { + return other != null && + other.GetType().Equals(this.GetType()) && + Source.SequenceEqual(other.Source); + } + + /// + /// Version 0 module source. + /// + /// Source code of module + public override int GetHashCode() + { + var sourceHash = Helpers.HashCode.GetHashCodeByteArray(this.Source); + return sourceHash + (int) this.GetVersion(); + } } internal static class VersionedModuleSourceFactory @@ -36,8 +57,7 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version }; internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? ContractName, DeserialErr? Error) output) { - (uint?, DeserialErr?) version = (null, null); - var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out version); + var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out var version); if (!versionSuccess) { output = (null, version.Item2); @@ -53,8 +73,8 @@ internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? Co if (version.Item1 == 0) { output = (ModuleV0.From(rest), null); return true; - } else if (version.Item1== 1) { - output = (ModuleV0.From(rest), null); + } else if (version.Item1 == 1) { + output = (ModuleV1.From(rest), null); return true; } else { output = (null, DeserialErr.InvalidModuleVersion); diff --git a/tests/UnitTests/Transactions/DeployModule.cs b/tests/UnitTests/Transactions/DeployModule.cs index f69d6ba1..188fa49e 100644 --- a/tests/UnitTests/Transactions/DeployModule.cs +++ b/tests/UnitTests/Transactions/DeployModule.cs @@ -46,12 +46,34 @@ public void ToBytes_ReturnsCorrectValue() [Fact] public void ToBytes_InverseOfFromBytes() { - // The expected payload was generated using the Concordium Rust SDK. var moduleBytes = CreateDeployModule().ToBytes(); - (DeployModule?, DeserialErr?) module = (null, null); - var deserialSuccess = DeployModule.TryDeserial(moduleBytes, out module); + var deserialSuccess = DeployModule.TryDeserial(moduleBytes, out var module); + + CreateDeployModule().Should().Be(module.Item1); + } + + [Fact] + public void Equality_BasicEqualityAndNull() + { + var v1_1 = ModuleV1.FromHex("00"); + var v1_2 = ModuleV1.FromHex("00"); + + v1_1.Should().Be(v1_2); + v1_1.Should().NotBe(null); + + v1_1 = null; + + v1_1.Should().Be(null); + v1_1.Should().NotBe(v1_2); + } + + [Fact] + public void Equality_DifferentVersionsNotEqual() + { + var v0 = ModuleV0.FromHex("00"); + var v1 = ModuleV1.FromHex("00"); - CreateDeployModule().Should().BeEquivalentTo(module.Item1); + v0.Should().NotBe(v1); } } From 8860563807533c5f56586d92953c31c157cb41e1 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 14 Nov 2023 16:56:28 +0100 Subject: [PATCH 07/21] Added deserialization of TransferWithMemo --- src/Helpers/Deserialization.cs | 33 +++++++++-- src/Transactions/DeployModule.cs | 17 ++++-- src/Transactions/Transfer.cs | 47 +++++++++++++-- src/Transactions/TransferWithMemo.cs | 58 +++++++++++++++++-- src/Types/AccountAddress.cs | 16 +++++ src/Types/CcdAmount.cs | 24 ++++++++ src/Types/OnChainData.cs | 24 ++++++++ src/Types/VersionedModuleSource.cs | 27 ++------- tests/UnitTests/Transactions/DeployModule.cs | 2 - tests/UnitTests/Transactions/TransferTests.cs | 10 ++++ 10 files changed, 215 insertions(+), 43 deletions(-) diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs index acf12195..018f4894 100644 --- a/src/Helpers/Deserialization.cs +++ b/src/Helpers/Deserialization.cs @@ -11,8 +11,11 @@ public enum DeserialErr InvalidModuleVersion, InvalidTransactionType, InternalError, + InvalidLength, } + + /// /// Helpers for deserializing data. /// @@ -21,21 +24,41 @@ public static class Deserial /// /// Creates a uint from a byte array. /// - public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, DeserialErr? Error) output) + public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, String? Error) output) { - var offset_input = input.Skip(offset).ToArray(); - - if (offset_input.Length < 4) { - output = (null, DeserialErr.TooShort); + if (input.Length < 4) { + var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; + output = (null, msg); return false; } + var offset_input = input.Skip(offset).ToArray(); + var bytes = offset_input.Take(4).ToArray(); output = (BinaryPrimitives.ReadUInt32BigEndian(bytes), null); return true; } + /// + /// Creates a uint from a byte array. + /// + public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, String? Error) output) + { + if (input.Length < 8) { + var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; + output = (null, msg); + return false; + } + + var offset_input = input.Skip(offset).ToArray(); + + var bytes = offset_input.Take(8).ToArray(); + + output = (BinaryPrimitives.ReadUInt64BigEndian(bytes), null); + return true; + } + // TODO: Debug tool remove private static void PrintBytes(String msg, byte[] bytes) { Console.WriteLine(msg); diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 5c80a324..464b77ad 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -25,25 +25,32 @@ private static byte[] Serialize(VersionedModuleSource module) return memoryStream.ToArray(); } - /// /// Create a "deploy module" payload from a serialized as bytes. /// /// The "deploy module" payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (DeployModule? ContractName, DeserialErr? Error) output) { - var deserialSuccess = VersionedModuleSourceFactory.TryDeserialize(bytes.Skip(1).ToArray(), out var module); + public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, String? Error) output) { + if (bytes.Length <= 9) { + var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least 9, found {bytes.Length}"; + output = (null, msg); + return false; + } + + var deserialSuccess = VersionedModuleSourceFactory.TryDeserial(bytes.Skip(1).ToArray(), out var module); if (!deserialSuccess) { output = (null, module.Item2); return false; }; if (bytes[0] != TransactionType) { - output = (null, DeserialErr.InvalidTransactionType); + var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; } output = (new DeployModule(module.Item1), null); - return false; + return true; } public override ulong GetTransactionSpecificCost() => 300; diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index 20206820..d0a0b756 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -16,6 +16,11 @@ public sealed record Transfer(CcdAmount Amount, AccountAddress Receiver) : Accou /// private const byte TransactionType = (byte)Types.TransactionType.Transfer; + /// + /// The length of the payload serialized. + /// + private const uint BytesLength = sizeof(TransactionType) + AccountAddress.BytesLength + CcdAmount.BytesLength; + /// /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. /// @@ -23,16 +28,50 @@ public sealed record Transfer(CcdAmount Amount, AccountAddress Receiver) : Accou /// Address of the receiver account to which the amount will be sent. private static byte[] Serialize(CcdAmount amount, AccountAddress receiver) { - using var memoryStream = new MemoryStream((int)( - sizeof(TransactionType) + - AccountAddress.BytesLength + - CcdAmount.BytesLength)); + using var memoryStream = new MemoryStream((int)(BytesLength)); memoryStream.WriteByte(TransactionType); memoryStream.Write(receiver.ToBytes()); memoryStream.Write(amount.ToBytes()); return memoryStream.ToArray(); } + /// + /// Create a "transfer" payload from a serialized as bytes. + /// + /// The "transfer" payload as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (Transfer? , String? Error) output) { + if (bytes.Length != BytesLength) { + var msg = $"Invalid length in `Transfer.TryDeserial`. Expected {BytesLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + if (bytes[0] != TransactionType) { + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; + }; + + var accountBytes = bytes.Skip(1).Take((int) AccountAddress.BytesLength).ToArray(); + var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); + + if (!accDeserial) { + output = (null, account.Item2); + return false; + }; + + var amountBytes = bytes.Skip((int) AccountAddress.BytesLength + 1).ToArray(); + var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); + + if (!amountDeserial) { + output = (null, amount.Item2); + return false; + }; + + output = (new Transfer(amount.Item1.Value, account.Item1), null); + return false; + } + public override ulong GetTransactionSpecificCost() => 300; public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver); diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 390498a4..43671eb5 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -18,6 +18,9 @@ public sealed record TransferWithMemo(CcdAmount Amount, AccountAddress Receiver, /// private const byte TransactionType = (byte)Types.TransactionType.TransferWithMemo; + private const uint BytesLength = sizeof(TransactionType) + AccountAddress.BytesLength + OnChainData.MaxLength + CcdAmount.BytesLength; + + /// /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. /// @@ -26,11 +29,7 @@ public sealed record TransferWithMemo(CcdAmount Amount, AccountAddress Receiver, /// Memo to include with the transaction. private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnChainData memo) { - using var memoryStream = new MemoryStream((int)( - sizeof(TransactionType) + - AccountAddress.BytesLength + - OnChainData.MaxLength + - CcdAmount.BytesLength)); + using var memoryStream = new MemoryStream((int)(BytesLength)); memoryStream.WriteByte(TransactionType); memoryStream.Write(receiver.ToBytes()); memoryStream.Write(memo.ToBytes()); @@ -38,6 +37,55 @@ private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnCha return memoryStream.ToArray(); } + /// + /// Create a "transfer" payload from a serialized as bytes. + /// + /// The "transfer" payload as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (TransferWithMemo? , String? Error) output) { + if (bytes.Length != BytesLength) { + var msg = $"Invalid length in `TransferWithMemo.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + if (bytes[0] != TransactionType) { + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; + }; + + var accountLength = (int) AccountAddress.BytesLength; + var amountLength = (int) CcdAmount.BytesLength; + var memoLength = (int) 1 - accountLength - amountLength; + + var accountBytes = bytes.Skip(1).Take(accountLength).ToArray(); + var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); + + if (!accDeserial) { + output = (null, account.Item2); + return false; + }; + + var memoBytes = bytes.Skip(1 + accountLength).Take(memoLength).ToArray(); + var memoDeserial = OnChainData.TryDeserial(memoBytes, out var memo); + + if (!memoDeserial) { + output = (null, memo.Item2); + return false; + }; + + var amountBytes = bytes.Skip(1 + accountLength + memoLength).Take(amountLength).ToArray(); + var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); + + if (!amountDeserial) { + output = (null, amount.Item2); + return false; + }; + + output = (new TransferWithMemo(amount.Item1.Value, account.Item1, memo.Item1), null); + return false; + } + public override ulong GetTransactionSpecificCost() => 300; public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver, this.Memo); diff --git a/src/Types/AccountAddress.cs b/src/Types/AccountAddress.cs index 49873d2a..88b5985d 100644 --- a/src/Types/AccountAddress.cs +++ b/src/Types/AccountAddress.cs @@ -216,6 +216,22 @@ public Grpc.V2.AccountAddress ToProto() => public Grpc.V2.AccountIdentifierInput ToAccountIdentifierInput() => new() { Address = this.ToProto() }; + /// + /// Create an account address from a serialized as bytes. + /// + /// The account address as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (AccountAddress? accountAddress , String? Error) output) { + if (bytes.Length != 32) { + var msg = $"Invalid length of input in `AccountAddress.TryDeserial`. Expected 32, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + output = (new AccountAddress(bytes.ToArray()), null); + return true; + } + public bool Equals(AccountAddress? other) => other is not null && this._value.SequenceEqual(other._value); public override int GetHashCode() => Helpers.HashCode.GetHashCodeByteArray(this._value); diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs index 0bc321af..f7b2bd32 100644 --- a/src/Types/CcdAmount.cs +++ b/src/Types/CcdAmount.cs @@ -110,6 +110,30 @@ public static CcdAmount FromCcd(ulong ccd) } } + /// + /// Create a CCD amount from a serialized as bytes. + /// + /// The CCD amount as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress , String? Error) output) { + if (bytes.Length != BytesLength) { + var msg = $"Invalid length of input in `CcdAmount.TryDeserial`. Expected {BytesLength}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + // This call also verifies the length + var U64Deserial = Helpers.Deserial.TryDeserialU64(bytes, 0, out var amount); + + if (!U64Deserial) { + output = (null, amount.Item2); + return false; + }; + + output = (new CcdAmount(amount.Item1.Value), null); + return true; + } + /// /// Copies the CCD amuunt represented in big-endian format to byte array. /// diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index 63d9ff8b..863d03ff 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -132,6 +132,30 @@ public byte[] ToBytes() /// public override string ToString() => Convert.ToHexString(this._value).ToLowerInvariant(); + /// + /// Create an account address from a serialized as bytes. + /// + /// The account address as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress , String? Error) output) { + if (bytes.Length == 0) { + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must be more than 0"; + output = (null, msg); + return false; + }; + + var size = (int) bytes.First(); + + if (bytes.Length != size+1) { + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Expected array of size {size+1}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + + output = (new OnChainData(bytes.Skip(1).ToArray()), null); + return true; + } + public bool Equals(OnChainData? other) => other is not null && this._value.SequenceEqual(other._value); public override int GetHashCode() => Helpers.HashCode.GetHashCodeByteArray(this._value); diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index f69fa194..aa8f268a 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -20,11 +20,7 @@ internal byte[] ToBytes() { return memoryStream.ToArray(); } - //public override bool Equals(object obj) - //{ - // return Equals(obj as VersionedModuleSource); - //} - + /// Check for equality. public virtual bool Equals(VersionedModuleSource? other) { return other != null && @@ -32,10 +28,7 @@ public virtual bool Equals(VersionedModuleSource? other) Source.SequenceEqual(other.Source); } - /// - /// Version 0 module source. - /// - /// Source code of module + /// Gets hash code. public override int GetHashCode() { var sourceHash = Helpers.HashCode.GetHashCodeByteArray(this.Source); @@ -56,7 +49,7 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version .ModuleCase) }; - internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? ContractName, DeserialErr? Error) output) { + internal static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, String? Error) output) { var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out var version); if (!versionSuccess) { @@ -64,7 +57,7 @@ internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? Co return false; } if (bytes.Length < 8) { - output = (null, DeserialErr.TooShort); + output = (null, "The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short"); return false; } @@ -77,7 +70,7 @@ internal static bool TryDeserialize(byte[] bytes, out (VersionedModuleSource? Co output = (ModuleV1.From(rest), null); return true; } else { - output = (null, DeserialErr.InvalidModuleVersion); + output = (null, $"Invalid module version byte, expected 0 or 1 but found {version.Item1}"); return false; }; } @@ -176,13 +169,3 @@ public static ModuleV1 FromHex(string hexString) } } } - -/// -/// Thrown when a matched enum value could not be handled in a switch statement. -/// -public sealed class InvalidModuleVersion : Exception -{ - internal InvalidModuleVersion(uint versionByte) : - base($"Unknown version byte: {versionByte}") - { } -} diff --git a/tests/UnitTests/Transactions/DeployModule.cs b/tests/UnitTests/Transactions/DeployModule.cs index 188fa49e..9b08acab 100644 --- a/tests/UnitTests/Transactions/DeployModule.cs +++ b/tests/UnitTests/Transactions/DeployModule.cs @@ -1,6 +1,4 @@ -using System; using Concordium.Sdk.Transactions; -using Concordium.Sdk.Helpers; using Concordium.Sdk.Types; using FluentAssertions; using Xunit; diff --git a/tests/UnitTests/Transactions/TransferTests.cs b/tests/UnitTests/Transactions/TransferTests.cs index f9615066..3d7c20bb 100644 --- a/tests/UnitTests/Transactions/TransferTests.cs +++ b/tests/UnitTests/Transactions/TransferTests.cs @@ -72,6 +72,16 @@ public void ToBytes_ReturnsCorrectValue() CreateTransfer().ToBytes().Should().BeEquivalentTo(expectedBytes); } + [Fact] + public void ToBytes_InverseOfFromBytes() + { + var transferBytes = CreateTransfer().ToBytes(); + + var deserialSuccess = Transfer.TryDeserial(transferBytes, out var transfer); + + CreateTransfer().Should().Be(transfer.Item1); + } + [Fact] public void Prepare_ThenSign_ProducesCorrectSignatures() { From 134a4ea106216d61873d673b2889244a41e8f823 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 21 Nov 2023 10:12:21 +0100 Subject: [PATCH 08/21] Deserial now works for `TransferWithMemo` and `RegisterData` --- src/Helpers/Deserialization.cs | 90 ++++++++++--------- src/Transactions/AccountSignatureMap.cs | 14 +-- src/Transactions/AccountTransactionHeader.cs | 8 +- src/Transactions/AccountTransactionPayload.cs | 45 +++++----- .../AccountTransactionSignature.cs | 5 +- src/Transactions/DeployModule.cs | 21 +++-- src/Transactions/RegisterData.cs | 34 +++++++ src/Transactions/SignedAccountTransaction.cs | 8 +- src/Transactions/Transfer.cs | 31 ++++--- src/Transactions/TransferWithMemo.cs | 55 ++++++------ src/Types/AccountAddress.cs | 6 +- src/Types/BlockItem.cs | 44 +++++---- src/Types/CcdAmount.cs | 15 ++-- src/Types/OnChainData.cs | 39 +++++--- src/Types/VersionedModuleSource.cs | 53 ++++++----- tests/UnitTests/Transactions/DeployModule.cs | 4 +- .../Transactions/RegisterDataTests.cs | 8 ++ .../Transactions/TransferWithMemoTests.cs | 8 ++ 18 files changed, 300 insertions(+), 188 deletions(-) diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs index 018f4894..e351991a 100644 --- a/src/Helpers/Deserialization.cs +++ b/src/Helpers/Deserialization.cs @@ -3,71 +3,81 @@ namespace Concordium.Sdk.Helpers; /// -/// Error on deserialization. +/// Helpers for deserializing data. /// -public enum DeserialErr +public static class Deserial { - TooShort, - InvalidModuleVersion, - InvalidTransactionType, - InternalError, - InvalidLength, -} + /// + /// Creates a ushort from a byte array. + /// + public static bool TryDeserialU16(byte[] input, int offset, out (ushort? Uint, string? Error) output) + { + if (input.Length < sizeof(ushort)) + { + var msg = $"Invalid length in TryDeserialU32. Must be longer than {sizeof(ushort)}, but was {input.Length}"; + output = (null, msg); + return false; + } + var offset_input = input.Skip(offset).ToArray(); + var bytes = offset_input.Take(sizeof(ushort)).ToArray(); + + output = (BinaryPrimitives.ReadUInt16BigEndian(bytes), null); + return true; + } -/// -/// Helpers for deserializing data. -/// -public static class Deserial -{ /// /// Creates a uint from a byte array. /// - public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, String? Error) output) + public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, string? Error) output) { - if (input.Length < 4) { - var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; - output = (null, msg); - return false; - } + if (input.Length < sizeof(uint)) + { + var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; + output = (null, msg); + return false; + } - var offset_input = input.Skip(offset).ToArray(); + var offset_input = input.Skip(offset).ToArray(); - var bytes = offset_input.Take(4).ToArray(); + var bytes = offset_input.Take(sizeof(uint)).ToArray(); output = (BinaryPrimitives.ReadUInt32BigEndian(bytes), null); - return true; + return true; } /// /// Creates a uint from a byte array. /// - public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, String? Error) output) + public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, string? Error) output) { - if (input.Length < 8) { - var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; - output = (null, msg); - return false; - } + if (input.Length < sizeof(ulong)) + { + var msg = $"Invalid length in TryDeserialU32. Must be longer than {sizeof(ulong)}, but was {input.Length}"; + output = (null, msg); + return false; + } - var offset_input = input.Skip(offset).ToArray(); + var offset_input = input.Skip(offset).ToArray(); - var bytes = offset_input.Take(8).ToArray(); + var bytes = offset_input.Take(sizeof(ulong)).ToArray(); output = (BinaryPrimitives.ReadUInt64BigEndian(bytes), null); - return true; + return true; } - // TODO: Debug tool remove - private static void PrintBytes(String msg, byte[] bytes) { - Console.WriteLine(msg); - foreach (byte b in bytes) { - Console.Write(b); - Console.Write(" "); - } - Console.Write("\n"); - } + // TODO: Debug tool remove + public static void PrintBytes(string msg, byte[] bytes) + { + Console.WriteLine(msg); + foreach (var b in bytes) + { + Console.Write(b); + Console.Write(" "); + } + Console.Write("\n"); + } } diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index 2b27200a..68414525 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -57,13 +57,14 @@ public Grpc.V2.AccountSignatureMap ToProto() return accountSignatureMap; } - internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) { + internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) + { var dict = new Dictionary(); foreach (var s in map.Signatures) { - dict.Add(new AccountKeyIndex((byte) s.Key), s.Value.Value.ToByteArray()); + dict.Add(new AccountKeyIndex((byte)s.Key), s.Value.Value.ToByteArray()); } - return AccountSignatureMap.Create(dict); + return Create(dict); } } @@ -108,12 +109,13 @@ public Grpc.V2.SignatureMap ToProto() return signatureMap; } - internal static UpdateInstructionSignatureMap From(Grpc.V2.SignatureMap map) { + internal static UpdateInstructionSignatureMap From(Grpc.V2.SignatureMap map) + { var dict = new Dictionary(); foreach (var s in map.Signatures) { - dict.Add(new UpdateKeysIndex((byte) s.Key), s.Value.Value.ToByteArray()); + dict.Add(new UpdateKeysIndex((byte)s.Key), s.Value.Value.ToByteArray()); } - return UpdateInstructionSignatureMap.Create(dict); + return Create(dict); } } diff --git a/src/Transactions/AccountTransactionHeader.cs b/src/Transactions/AccountTransactionHeader.cs index 6d6b918c..4ca90f5b 100644 --- a/src/Transactions/AccountTransactionHeader.cs +++ b/src/Transactions/AccountTransactionHeader.cs @@ -91,13 +91,11 @@ public Grpc.V2.AccountTransactionHeader ToProto() => /// /// Converts the account transaction header to its corresponding protocol buffer message instance. /// - internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader) { - return new AccountTransactionHeader( - AccountAddress.From(accountTransactionHeader.Sender), + internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader) => new( + AccountAddress.From(accountTransactionHeader.Sender), AccountSequenceNumber.From(accountTransactionHeader.SequenceNumber), Expiry.From(accountTransactionHeader.Expiry.Value), EnergyAmount.From(accountTransactionHeader.EnergyAmount), - new PayloadSize((uint) accountTransactionHeader.CalculateSize()) + new PayloadSize((uint)accountTransactionHeader.CalculateSize()) ); - } } diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 1de6518e..96501905 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -1,5 +1,5 @@ -using Concordium.Sdk.Types; using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Types; using PayloadCase = Concordium.Grpc.V2.AccountTransactionPayload.PayloadOneofCase; namespace Concordium.Sdk.Transactions; @@ -50,24 +50,27 @@ Expiry expiry public Grpc.V2.AccountTransactionPayload ToProto() => new() { RawPayload = Google.Protobuf.ByteString.CopyFrom(this.ToBytes()) }; - internal static AccountTransactionPayload From(Grpc.V2.AccountTransactionPayload payload) { - return payload.PayloadCase switch { - PayloadCase.TransferWithMemo => new TransferWithMemo( - CcdAmount.From(payload.TransferWithMemo.Amount), - AccountAddress.From(payload.TransferWithMemo.Receiver), - OnChainData.From(payload.TransferWithMemo.Memo) - ), - PayloadCase.Transfer => new Transfer( - CcdAmount.From(payload.Transfer.Amount), - AccountAddress.From(payload.Transfer.Receiver) - ), - PayloadCase.RegisterData => new RegisterData( - OnChainData.From(payload.RegisterData) - ), - PayloadCase.DeployModule => new DeployModule( - VersionedModuleSourceFactory.From(payload.DeployModule) - ), - _ => throw new MissingEnumException(payload.PayloadCase), - }; - } + internal static AccountTransactionPayload From(Grpc.V2.AccountTransactionPayload payload) => payload.PayloadCase switch + { + PayloadCase.TransferWithMemo => new TransferWithMemo( + CcdAmount.From(payload.TransferWithMemo.Amount), + AccountAddress.From(payload.TransferWithMemo.Receiver), + OnChainData.From(payload.TransferWithMemo.Memo) + ), + PayloadCase.Transfer => new Transfer( + CcdAmount.From(payload.Transfer.Amount), + AccountAddress.From(payload.Transfer.Receiver) + ), + PayloadCase.RegisterData => new RegisterData( + OnChainData.From(payload.RegisterData) + ), + PayloadCase.DeployModule => new DeployModule( + VersionedModuleSourceFactory.From(payload.DeployModule) + ), + PayloadCase.None => throw new NotImplementedException(), + PayloadCase.RawPayload => throw new NotImplementedException(), + PayloadCase.InitContract => throw new NotImplementedException(), + PayloadCase.UpdateContract => throw new NotImplementedException(), + _ => throw new MissingEnumException(payload.PayloadCase), + }; } diff --git a/src/Transactions/AccountTransactionSignature.cs b/src/Transactions/AccountTransactionSignature.cs index dc1b1745..0a764027 100644 --- a/src/Transactions/AccountTransactionSignature.cs +++ b/src/Transactions/AccountTransactionSignature.cs @@ -76,9 +76,10 @@ public Grpc.V2.AccountTransactionSignature ToProto() internal static AccountTransactionSignature From(Grpc.V2.AccountTransactionSignature signature) { var dict = new Dictionary(); - foreach (var s in signature.Signatures) { + foreach (var s in signature.Signatures) + { dict.Add( - new AccountCredentialIndex((byte) s.Key), + new AccountCredentialIndex((byte)s.Key), AccountSignatureMap.From(s.Value) ); } diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 464b77ad..ae782231 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -1,4 +1,3 @@ -using Concordium.Sdk.Helpers; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -30,26 +29,30 @@ private static byte[] Serialize(VersionedModuleSource module) /// /// The "deploy module" payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, String? Error) output) { - if (bytes.Length <= 9) { - var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least 9, found {bytes.Length}"; + public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, string? Error) output) + { + if (bytes.Length <= 9) + { + var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least 9, found {bytes.Length}"; output = (null, msg); return false; } var deserialSuccess = VersionedModuleSourceFactory.TryDeserial(bytes.Skip(1).ToArray(), out var module); - if (!deserialSuccess) { - output = (null, module.Item2); + if (!deserialSuccess) + { + output = (null, module.Error); return false; }; - if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; } - output = (new DeployModule(module.Item1), null); + output = (new DeployModule(module.VersionedModuleSource), null); return true; } diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index 506cd258..c7a7ce4b 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -31,5 +31,39 @@ private static byte[] Serialize(OnChainData data) return memoryStream.ToArray(); } + /// + /// Create a "register data" payload from a serialized as bytes. + /// + /// The payload as bytes. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (RegisterData?, string? Error) output) + { + var minSize = sizeof(TransactionType); + if (bytes.Length < minSize) + { + var msg = $"Invalid length in `TransferWithMemo.TryDeserial`. Expected at least {minSize}, found {bytes.Length}"; + output = (null, msg); + return false; + }; + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; + }; + + var memoBytes = bytes.Skip(sizeof(TransactionType)).ToArray(); + var memoDeserial = OnChainData.TryDeserial(memoBytes, out var memo); + + if (!memoDeserial) + { + output = (null, memo.Error); + return false; + }; + + output = (new RegisterData(memo.accountAddress), null); + return true; + } + public override byte[] ToBytes() => Serialize(this.Data); } diff --git a/src/Transactions/SignedAccountTransaction.cs b/src/Transactions/SignedAccountTransaction.cs index c2bd5b07..fcded504 100644 --- a/src/Transactions/SignedAccountTransaction.cs +++ b/src/Transactions/SignedAccountTransaction.cs @@ -20,10 +20,10 @@ public record SignedAccountTransaction( AccountTransactionHeader Header, AccountTransactionPayload Payload, AccountTransactionSignature Signature -): BlockItemType +) : BlockItemType { /// Converts this type to the equivalent protocol buffer type. - public Grpc.V2.AccountTransaction ToProto() => + public AccountTransaction ToProto() => new() { Header = this.Header.ToProto(), @@ -31,13 +31,11 @@ public Grpc.V2.AccountTransaction ToProto() => Signature = this.Signature.ToProto(), }; - internal static SignedAccountTransaction From(Grpc.V2.AccountTransaction accountTransaction) { - return new SignedAccountTransaction( + internal static SignedAccountTransaction From(AccountTransaction accountTransaction) => new( AccountTransactionHeader.From(accountTransaction.Header), AccountTransactionPayload.From(accountTransaction.Payload), AccountTransactionSignature.From(accountTransaction.Signature) ); - } /// /// Converts the signed account transaction to a protocol buffer diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index d0a0b756..dcbcba9d 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -28,7 +28,7 @@ public sealed record Transfer(CcdAmount Amount, AccountAddress Receiver) : Accou /// Address of the receiver account to which the amount will be sent. private static byte[] Serialize(CcdAmount amount, AccountAddress receiver) { - using var memoryStream = new MemoryStream((int)(BytesLength)); + using var memoryStream = new MemoryStream((int)BytesLength); memoryStream.WriteByte(TransactionType); memoryStream.Write(receiver.ToBytes()); memoryStream.Write(amount.ToBytes()); @@ -40,36 +40,41 @@ private static byte[] Serialize(CcdAmount amount, AccountAddress receiver) /// /// The "transfer" payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (Transfer? , String? Error) output) { - if (bytes.Length != BytesLength) { + public static bool TryDeserial(byte[] bytes, out (Transfer?, string? Error) output) + { + if (bytes.Length != BytesLength) + { var msg = $"Invalid length in `Transfer.TryDeserial`. Expected {BytesLength}, found {bytes.Length}"; output = (null, msg); return false; }; - if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; }; - var accountBytes = bytes.Skip(1).Take((int) AccountAddress.BytesLength).ToArray(); + var accountBytes = bytes.Skip(1).Take((int)AccountAddress.BytesLength).ToArray(); var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); - if (!accDeserial) { - output = (null, account.Item2); + if (!accDeserial) + { + output = (null, account.Error); return false; }; - var amountBytes = bytes.Skip((int) AccountAddress.BytesLength + 1).ToArray(); + var amountBytes = bytes.Skip((int)AccountAddress.BytesLength + 1).ToArray(); var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); - if (!amountDeserial) { - output = (null, amount.Item2); + if (!amountDeserial) + { + output = (null, amount.Error); return false; }; - output = (new Transfer(amount.Item1.Value, account.Item1), null); - return false; + output = (new Transfer(amount.accountAddress.Value, account.accountAddress), null); + return true; } public override ulong GetTransactionSpecificCost() => 300; diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 43671eb5..75129e18 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -18,9 +18,6 @@ public sealed record TransferWithMemo(CcdAmount Amount, AccountAddress Receiver, /// private const byte TransactionType = (byte)Types.TransactionType.TransferWithMemo; - private const uint BytesLength = sizeof(TransactionType) + AccountAddress.BytesLength + OnChainData.MaxLength + CcdAmount.BytesLength; - - /// /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. /// @@ -29,7 +26,7 @@ public sealed record TransferWithMemo(CcdAmount Amount, AccountAddress Receiver, /// Memo to include with the transaction. private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnChainData memo) { - using var memoryStream = new MemoryStream((int)(BytesLength)); + using var memoryStream = new MemoryStream((int)(sizeof(TransactionType) + CcdAmount.BytesLength + AccountAddress.BytesLength + OnChainData.MaxLength)); memoryStream.WriteByte(TransactionType); memoryStream.Write(receiver.ToBytes()); memoryStream.Write(memo.ToBytes()); @@ -38,52 +35,60 @@ private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnCha } /// - /// Create a "transfer" payload from a serialized as bytes. + /// Create a "transfer with memo" payload from a serialized as bytes. /// - /// The "transfer" payload as bytes. + /// The payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (TransferWithMemo? , String? Error) output) { - if (bytes.Length != BytesLength) { - var msg = $"Invalid length in `TransferWithMemo.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; + public static bool TryDeserial(byte[] bytes, out (TransferWithMemo?, string? Error) output) + { + var minSize = sizeof(TransactionType) + AccountAddress.BytesLength + CcdAmount.BytesLength; + if (bytes.Length < minSize) + { + var msg = $"Invalid length in `TransferWithMemo.TryDeserial`. Expected at least {minSize}, found {bytes.Length}"; output = (null, msg); return false; }; - if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; }; - var accountLength = (int) AccountAddress.BytesLength; - var amountLength = (int) CcdAmount.BytesLength; - var memoLength = (int) 1 - accountLength - amountLength; + var trxTypeLength = sizeof(TransactionType); + var accountLength = (int)AccountAddress.BytesLength; + var amountLength = (int)CcdAmount.BytesLength; + var memoLength = bytes.Length - trxTypeLength - accountLength - amountLength; - var accountBytes = bytes.Skip(1).Take(accountLength).ToArray(); + var accountBytes = bytes.Skip(trxTypeLength).Take(accountLength).ToArray(); var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); - if (!accDeserial) { - output = (null, account.Item2); + if (!accDeserial) + { + output = (null, account.Error); return false; }; - var memoBytes = bytes.Skip(1 + accountLength).Take(memoLength).ToArray(); + var memoBytes = bytes.Skip(trxTypeLength + accountLength).Take(memoLength).ToArray(); var memoDeserial = OnChainData.TryDeserial(memoBytes, out var memo); - if (!memoDeserial) { - output = (null, memo.Item2); + if (!memoDeserial) + { + output = (null, memo.Error); return false; }; - var amountBytes = bytes.Skip(1 + accountLength + memoLength).Take(amountLength).ToArray(); + var amountBytes = bytes.Skip(trxTypeLength + accountLength + memoLength).Take(amountLength).ToArray(); var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); - if (!amountDeserial) { - output = (null, amount.Item2); + if (!amountDeserial) + { + output = (null, amount.Error); return false; }; - output = (new TransferWithMemo(amount.Item1.Value, account.Item1, memo.Item1), null); - return false; + output = (new TransferWithMemo(amount.accountAddress.Value, account.accountAddress, memo.accountAddress), null); + return true; } public override ulong GetTransactionSpecificCost() => 300; diff --git a/src/Types/AccountAddress.cs b/src/Types/AccountAddress.cs index 88b5985d..bc5dc4e0 100644 --- a/src/Types/AccountAddress.cs +++ b/src/Types/AccountAddress.cs @@ -221,8 +221,10 @@ public Grpc.V2.AccountIdentifierInput ToAccountIdentifierInput() => /// /// The account address as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (AccountAddress? accountAddress , String? Error) output) { - if (bytes.Length != 32) { + public static bool TryDeserial(byte[] bytes, out (AccountAddress? accountAddress, string? Error) output) + { + if (bytes.Length != 32) + { var msg = $"Invalid length of input in `AccountAddress.TryDeserial`. Expected 32, found {bytes.Length}"; output = (null, msg); return false; diff --git a/src/Types/BlockItem.cs b/src/Types/BlockItem.cs index ddec6c2b..f3ba33c7 100644 --- a/src/Types/BlockItem.cs +++ b/src/Types/BlockItem.cs @@ -1,8 +1,8 @@ using Concordium.Sdk.Exceptions; using Concordium.Sdk.Transactions; +using BlockItemCase = Concordium.Grpc.V2.BlockItem.BlockItemOneofCase; using CredentialDeploymentPayloadCase = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; using UpdateInstructionPayloadCase = Concordium.Grpc.V2.UpdateInstructionPayload.PayloadOneofCase; -using BlockItemCase = Concordium.Grpc.V2.BlockItem.BlockItemOneofCase; namespace Concordium.Sdk.Types; @@ -15,14 +15,16 @@ namespace Concordium.Sdk.Types; /// The payload of the UpdateInstruction. Can currently only be a `RawPayload` public record UpdateInstruction( UpdateInstructionSignatureMap SignatureMap, - UpdateInstructionHeader Header, + UpdateInstructionHeader Header, IUpdateInstructionPayload Payload -): BlockItemType { +) : BlockItemType +{ internal static UpdateInstruction From(Grpc.V2.UpdateInstruction updateInstruction) => - new UpdateInstruction( + new( UpdateInstructionSignatureMap.From(updateInstruction.Signatures), UpdateInstructionHeader.From(updateInstruction.Header), - updateInstruction.Payload.PayloadCase switch { + updateInstruction.Payload.PayloadCase switch + { UpdateInstructionPayloadCase.RawPayload => new UpdateInstructionPayloadRaw(updateInstruction.Payload.RawPayload.ToByteArray()), UpdateInstructionPayloadCase.None => throw new NotImplementedException(), _ => throw new MissingEnumException(updateInstruction.Payload.PayloadCase), @@ -35,12 +37,13 @@ internal static UpdateInstruction From(Grpc.V2.UpdateInstruction updateInstructi /// When the update takes effect. /// Latest time the update instruction can included in a block. public record UpdateInstructionHeader( - UpdateSequenceNumber SequenceNumber, - TransactionTime EffectiveTime, + UpdateSequenceNumber SequenceNumber, + TransactionTime EffectiveTime, TransactionTime Timeout -) { +) +{ internal static UpdateInstructionHeader From(Grpc.V2.UpdateInstructionHeader header) => - new UpdateInstructionHeader( + new( UpdateSequenceNumber.From(header.SequenceNumber), TransactionTime.From(header.EffectiveTime), TransactionTime.From(header.Timeout) @@ -52,13 +55,14 @@ internal static UpdateInstructionHeader From(Grpc.V2.UpdateInstructionHeader hea /// Equivalent to `SequenceNumber` for account transactions. /// Update sequence numbers are per update type and the minimum value is 1. /// -public record UpdateSequenceNumber(UInt64 SequenceNumber) { +public record UpdateSequenceNumber(ulong SequenceNumber) +{ internal static UpdateSequenceNumber From(Grpc.V2.UpdateSequenceNumber sequenceNumber) => - new UpdateSequenceNumber(sequenceNumber.Value); + new(sequenceNumber.Value); } /// The payload for an UpdateInstruction. -public interface IUpdateInstructionPayload{} +public interface IUpdateInstructionPayload { } /// A raw payload encoded according to the format defined by the protocol. public sealed record UpdateInstructionPayloadRaw(byte[] RawPayload) : IUpdateInstructionPayload; @@ -70,11 +74,13 @@ public sealed record UpdateInstructionPayloadRaw(byte[] RawPayload) : IUpdateIns /// /// Latest time the credential deployment can included in a block. /// The payload of the credential deployment. -public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload): BlockItemType { +public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload) : BlockItemType +{ internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => - new CredentialDeployment( + new( TransactionTime.From(cred.MessageExpiry), - cred.PayloadCase switch { + cred.PayloadCase switch + { CredentialDeploymentPayloadCase.RawPayload => new CredentialPayloadRaw(cred.RawPayload.ToByteArray()), CredentialDeploymentPayloadCase.None => throw new NotImplementedException(), _ => throw new MissingEnumException(cred.PayloadCase), @@ -83,7 +89,7 @@ internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => } /// The payload of a Credential Deployment. -public interface ICredentialPayload{}; +public interface ICredentialPayload { }; /// /// A raw payload, which is just the encoded payload. @@ -97,12 +103,14 @@ public sealed record CredentialPayloadRaw(byte[] RawPayload) : ICredentialPayloa public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType) { internal static BlockItem From(Grpc.V2.BlockItem blockItem) => - new BlockItem( + new( TransactionHash.From(blockItem.Hash.ToString()), - blockItem.BlockItemCase switch { + blockItem.BlockItemCase switch + { BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), BlockItemCase.CredentialDeployment => CredentialDeployment.From(blockItem.CredentialDeployment), BlockItemCase.UpdateInstruction => UpdateInstruction.From(blockItem.UpdateInstruction), + BlockItemCase.None => throw new NotImplementedException(), _ => throw new MissingEnumException(blockItem.BlockItemCase), } ); diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs index f7b2bd32..166a4357 100644 --- a/src/Types/CcdAmount.cs +++ b/src/Types/CcdAmount.cs @@ -115,22 +115,25 @@ public static CcdAmount FromCcd(ulong ccd) /// /// The CCD amount as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress , String? Error) output) { - if (bytes.Length != BytesLength) { + public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress, string? Error) output) + { + if (bytes.Length != BytesLength) + { var msg = $"Invalid length of input in `CcdAmount.TryDeserial`. Expected {BytesLength}, found {bytes.Length}"; output = (null, msg); return false; }; // This call also verifies the length - var U64Deserial = Helpers.Deserial.TryDeserialU64(bytes, 0, out var amount); + var U64Deserial = Deserial.TryDeserialU64(bytes, 0, out var amount); - if (!U64Deserial) { - output = (null, amount.Item2); + if (!U64Deserial) + { + output = (null, amount.Error); return false; }; - output = (new CcdAmount(amount.Item1.Value), null); + output = (new CcdAmount(amount.Ulong.Value), null); return true; } diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index 863d03ff..87d1946b 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -12,6 +12,9 @@ namespace Concordium.Sdk.Types; /// public sealed record OnChainData : IEquatable { + /// + /// The maximum length of a bytearray passed to the constructor. + /// public const uint MaxLength = 256; /// @@ -137,22 +140,39 @@ public byte[] ToBytes() /// /// The account address as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress , String? Error) output) { - if (bytes.Length == 0) { - var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must be more than 0"; + public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress, string? Error) output) + { + if (bytes.Length == 0) + { + var msg = "Invalid length of input in `OnChainData.TryDeserial`. Length must be more than 0"; + output = (null, msg); + return false; + }; + + if (bytes.Length > sizeof(ushort) + MaxLength) + { + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must not be more than {sizeof(ushort) + MaxLength}"; output = (null, msg); return false; }; - var size = (int) bytes.First(); + var deserialSuccess = Deserial.TryDeserialU16(bytes, 0, out var sizeRead); + if (!deserialSuccess) + { + output = (null, sizeRead.Error); + return false; + }; + + var size = sizeRead.Uint + sizeof(ushort); - if (bytes.Length != size+1) { - var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Expected array of size {size+1}, found {bytes.Length}"; + if (bytes.Length != size) + { + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Expected array of size {size}, found {bytes.Length}"; output = (null, msg); return false; }; - output = (new OnChainData(bytes.Skip(1).ToArray()), null); + output = (new OnChainData(bytes.Skip(sizeof(ushort)).ToArray()), null); return true; } @@ -170,8 +190,5 @@ public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress , return From(memo.Value.ToByteArray()); } - internal static OnChainData From(Grpc.V2.RegisteredData registeredData) - { - return From(registeredData.Value.ToByteArray()); - } + internal static OnChainData From(Grpc.V2.RegisteredData registeredData) => From(registeredData.Value.ToByteArray()); } diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index aa8f268a..7430c8f1 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -6,33 +6,32 @@ namespace Concordium.Sdk.Types; /// /// Contains source code of a versioned module where inherited classes are concrete versions. /// -public abstract record VersionedModuleSource(byte[] Source) : IEquatable { +public abstract record VersionedModuleSource(byte[] Source) : IEquatable +{ internal const uint MaxLength = 8 * 65536; - internal uint BytesLength = 2 * sizeof(int) + (uint) Source.Length; + internal uint BytesLength = (2 * sizeof(int)) + (uint)Source.Length; internal abstract uint GetVersion(); - internal byte[] ToBytes() { - using var memoryStream = new MemoryStream((int)(BytesLength)); - memoryStream.Write(Serialization.ToBytes(GetVersion())); - memoryStream.Write(Serialization.ToBytes((uint) Source.Length)); - memoryStream.Write(Source); + internal byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.BytesLength); + memoryStream.Write(Serialization.ToBytes(this.GetVersion())); + memoryStream.Write(Serialization.ToBytes((uint)this.Source.Length)); + memoryStream.Write(this.Source); return memoryStream.ToArray(); } /// Check for equality. - public virtual bool Equals(VersionedModuleSource? other) - { - return other != null && + public virtual bool Equals(VersionedModuleSource? other) => other != null && other.GetType().Equals(this.GetType()) && - Source.SequenceEqual(other.Source); - } + this.Source.SequenceEqual(other.Source); /// Gets hash code. public override int GetHashCode() { var sourceHash = Helpers.HashCode.GetHashCodeByteArray(this.Source); - return sourceHash + (int) this.GetVersion(); + return sourceHash + (int)this.GetVersion(); } } @@ -49,28 +48,36 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version .ModuleCase) }; - internal static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, String? Error) output) { + internal static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) + { var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out var version); - if (!versionSuccess) { - output = (null, version.Item2); + if (!versionSuccess) + { + output = (null, version.Error); return false; } - if (bytes.Length < 8) { + if (bytes.Length < 8) + { output = (null, "The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short"); return false; } var rest = bytes.Skip(8).ToArray(); - if (version.Item1 == 0) { + if (version.Uint == 0) + { output = (ModuleV0.From(rest), null); return true; - } else if (version.Item1 == 1) { + } + else if (version.Uint == 1) + { output = (ModuleV1.From(rest), null); return true; - } else { - output = (null, $"Invalid module version byte, expected 0 or 1 but found {version.Item1}"); + } + else + { + output = (null, $"Invalid module version byte, expected 0 or 1 but found {version.Uint}"); return false; }; } @@ -82,7 +89,7 @@ internal static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? Versi /// Source code of module public sealed record ModuleV0(byte[] Source) : VersionedModuleSource(Source) { - override internal uint GetVersion() => 0; + internal override uint GetVersion() => 0; internal static ModuleV0 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV0 moduleSourceV0) => new(moduleSourceV0.Value.ToByteArray()); @@ -129,7 +136,7 @@ public static ModuleV0 FromHex(string hexString) /// Source code of module public sealed record ModuleV1(byte[] Source) : VersionedModuleSource(Source) { - override internal uint GetVersion() => 1; + internal override uint GetVersion() => 1; internal static ModuleV1 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV1 moduleSourceV1) => new(moduleSourceV1.Value.ToByteArray()); diff --git a/tests/UnitTests/Transactions/DeployModule.cs b/tests/UnitTests/Transactions/DeployModule.cs index 9b08acab..19220b32 100644 --- a/tests/UnitTests/Transactions/DeployModule.cs +++ b/tests/UnitTests/Transactions/DeployModule.cs @@ -29,7 +29,7 @@ public static DeployModule CreateDeployModule() public void ToBytes_ReturnsCorrectValue() { // The expected payload was generated using the Concordium Rust SDK. - var expectedBytes = new byte[] { + var expectedBytes = new byte[] { 0, 0, 0, 0, 1, 0, 0, 0, 86, 0, 97, 115, 109, 1, 0, 0, 0, 4, 5, 1, 112, 1, 1, 1, 5, 3, 1, 0, 16, 6, 25, 3, 127, 1, 65, 128, 128, 192, 0, 11, 127, 0, 65, 128, 128, 192, 0, 11, 127, 0, 65, 128, @@ -48,7 +48,7 @@ public void ToBytes_InverseOfFromBytes() var deserialSuccess = DeployModule.TryDeserial(moduleBytes, out var module); - CreateDeployModule().Should().Be(module.Item1); + CreateDeployModule().Should().Be(module.Module); } [Fact] diff --git a/tests/UnitTests/Transactions/RegisterDataTests.cs b/tests/UnitTests/Transactions/RegisterDataTests.cs index 01d6bc83..89b64785 100644 --- a/tests/UnitTests/Transactions/RegisterDataTests.cs +++ b/tests/UnitTests/Transactions/RegisterDataTests.cs @@ -27,6 +27,14 @@ public void ToBytes_ReturnsCorrectValue() CreateRegisterData().ToBytes().Should().BeEquivalentTo(expectedBytes); } + [Fact] + public void ToBytes_TryDeserialIsInverse() + { + var registerData = CreateRegisterData(); + var result = RegisterData.TryDeserial(registerData.ToBytes(), out var registerDataDeserial); + registerData.Should().Be(registerDataDeserial.Item1); + } + [Fact] public void Prepare_ThenSign_ProducesCorrectSignatures() { diff --git a/tests/UnitTests/Transactions/TransferWithMemoTests.cs b/tests/UnitTests/Transactions/TransferWithMemoTests.cs index a0850b92..18ebeb23 100644 --- a/tests/UnitTests/Transactions/TransferWithMemoTests.cs +++ b/tests/UnitTests/Transactions/TransferWithMemoTests.cs @@ -84,6 +84,14 @@ public void ToBytes_ReturnsCorrectValue() CreateTransferWithMemo().ToBytes().Should().BeEquivalentTo(expectedBytes); } + [Fact] + public void ToBytes_TryDeserialIsInverse() + { + var transfer = CreateTransferWithMemo(); + var transferResult = TransferWithMemo.TryDeserial(transfer.ToBytes(), out var transferDeserial); + transfer.Should().Be(transferDeserial.Item1); + } + [Fact] public void Prepare_ThenSign_ProducesCorrectSignatures() { From a6dda782763311ebfcd3476fb19103835e50f95c Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 21 Nov 2023 15:22:17 +0100 Subject: [PATCH 09/21] Moved `BlockItem` and added RawPayload --- src/Exceptions/DeserialException.cs | 12 ++ src/Transactions/AccountTransactionPayload.cs | 129 ++++++++++++++---- src/Transactions/BlockItem.cs | 27 ++++ src/Transactions/CredentialDeployment.cs | 36 +++++ src/Transactions/DeployModule.cs | 26 +++- .../PreparedAccountTransaction.cs | 5 +- src/Transactions/RawPayload.cs | 14 ++ src/Transactions/RegisterData.cs | 21 ++- src/Transactions/SignedAccountTransaction.cs | 1 - src/Transactions/Transfer.cs | 23 +++- src/Transactions/TransferWithMemo.cs | 23 +++- .../UpdateInstruction.cs} | 58 +------- tests/IntegrationTests/Tests.cs | 16 ++- .../Transactions/TransactionTestHelpers.cs | 10 +- 14 files changed, 302 insertions(+), 99 deletions(-) create mode 100644 src/Exceptions/DeserialException.cs create mode 100644 src/Transactions/BlockItem.cs create mode 100644 src/Transactions/CredentialDeployment.cs create mode 100644 src/Transactions/RawPayload.cs rename src/{Types/BlockItem.cs => Transactions/UpdateInstruction.cs} (51%) diff --git a/src/Exceptions/DeserialException.cs b/src/Exceptions/DeserialException.cs new file mode 100644 index 00000000..9813ce7a --- /dev/null +++ b/src/Exceptions/DeserialException.cs @@ -0,0 +1,12 @@ +namespace Concordium.Sdk.Exceptions; + +/// +/// Thrown when a matched enum value could not be handled in a switch statement. +/// +public sealed class DeserialException : Exception +{ + internal DeserialException(string errorMessage) : + base($"Deserialization error: {errorMessage}") + { } +} + diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 96501905..de1bbb07 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -13,32 +13,6 @@ namespace Concordium.Sdk.Transactions; /// public abstract record AccountTransactionPayload { - /// - /// Prepares the account transaction payload for signing. - /// - /// Address of the sender of the transaction. - /// Account sequence number to use for the transaction. - /// Expiration time of the transaction. - public PreparedAccountTransaction Prepare( - AccountAddress sender, - AccountSequenceNumber sequenceNumber, - Expiry expiry - ) => new(sender, sequenceNumber, expiry, this); - - /// - /// Gets the transaction specific cost for submitting this type of - /// transaction to the chain. - /// - /// This should reflect the transaction-specific costs defined here: - /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs - /// - /// Note that this is only part of the cost of a transaction, and - /// does not include costs associated with verification of signatures - /// as well as costs that are incurred at execution time, for instance - /// when initializing or updating a smart contract. - /// - public abstract ulong GetTransactionSpecificCost(); - /// /// Copies the on-chain data in the binary format expected by the node to a byte array. /// @@ -67,10 +41,109 @@ public Grpc.V2.AccountTransactionPayload ToProto() => PayloadCase.DeployModule => new DeployModule( VersionedModuleSourceFactory.From(payload.DeployModule) ), - PayloadCase.None => throw new NotImplementedException(), - PayloadCase.RawPayload => throw new NotImplementedException(), + PayloadCase.RawPayload => ParseRawPayload(payload.RawPayload), PayloadCase.InitContract => throw new NotImplementedException(), PayloadCase.UpdateContract => throw new NotImplementedException(), + PayloadCase.None => throw new NotImplementedException(), _ => throw new MissingEnumException(payload.PayloadCase), }; + + private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteString payload) + { + (AccountTransactionPayload?, string?) parsedPayload = (null, null); + + switch ((TransactionType)payload.First()) + { + case TransactionType.Transfer: + { + Transfer.TryDeserial(payload.ToArray(), out var output); + parsedPayload = output; + break; + } + case TransactionType.TransferWithMemo: + { + TransferWithMemo.TryDeserial(payload.ToArray(), out var output); + parsedPayload = output; + break; + } + case TransactionType.RegisterData: + { + RegisterData.TryDeserial(payload.ToArray(), out var output); + parsedPayload = output; + break; + } + case TransactionType.DeployModule: + { + DeployModule.TryDeserial(payload.ToArray(), out var output); + parsedPayload = output; + break; + } + + case TransactionType.InitContract: + break; + case TransactionType.Update: + break; + case TransactionType.AddBaker: + break; + case TransactionType.RemoveBaker: + break; + case TransactionType.UpdateBakerStake: + break; + case TransactionType.UpdateBakerRestakeEarnings: + break; + case TransactionType.UpdateBakerKeys: + break; + case TransactionType.UpdateCredentialKeys: + break; + case TransactionType.EncryptedAmountTransfer: + break; + case TransactionType.TransferToEncrypted: + break; + case TransactionType.TransferToPublic: + break; + case TransactionType.TransferWithSchedule: + break; + case TransactionType.UpdateCredentials: + break; + case TransactionType.EncryptedAmountTransferWithMemo: + break; + case TransactionType.TransferWithScheduleAndMemo: + break; + case TransactionType.ConfigureBaker: + break; + case TransactionType.ConfigureDelegation: + break; + default: + throw new NotImplementedException(); + }; + + if (parsedPayload.Item2 != null) + { + throw new DeserialException(parsedPayload.Item2); + } + return parsedPayload.Item1; + } + + /// + /// Prepares the account transaction payload for signing. Will throw an + /// exception if AccountTransaction is of subtype RawPayload. Should only + /// be used for testing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + internal PreparedAccountTransaction PrepareWithException( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry + ) => this switch + { + Transfer transfer => transfer.Prepare(sender, sequenceNumber, expiry), + TransferWithMemo transferWithMemo => transferWithMemo.Prepare(sender, sequenceNumber, expiry), + DeployModule deployModule => deployModule.Prepare(sender, sequenceNumber, expiry), + RegisterData registerData => registerData.Prepare(sender, sequenceNumber, expiry), + _ => throw new NotImplementedException(), + }; + } + diff --git a/src/Transactions/BlockItem.cs b/src/Transactions/BlockItem.cs new file mode 100644 index 00000000..ec2a0a13 --- /dev/null +++ b/src/Transactions/BlockItem.cs @@ -0,0 +1,27 @@ +using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Types; +using BlockItemCase = Concordium.Grpc.V2.BlockItem.BlockItemOneofCase; + +namespace Concordium.Sdk.Transactions; + +/// A block item. +/// The hash of the block item that identifies it to the chain. +/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. +public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType) +{ + internal static BlockItem From(Grpc.V2.BlockItem blockItem) => + new( + TransactionHash.From(blockItem.Hash.ToString()), + blockItem.BlockItemCase switch + { + BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), + BlockItemCase.CredentialDeployment => CredentialDeployment.From(blockItem.CredentialDeployment), + BlockItemCase.UpdateInstruction => UpdateInstruction.From(blockItem.UpdateInstruction), + BlockItemCase.None => throw new NotImplementedException(), + _ => throw new MissingEnumException(blockItem.BlockItemCase), + } + ); +} + +/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. +public abstract record BlockItemType; diff --git a/src/Transactions/CredentialDeployment.cs b/src/Transactions/CredentialDeployment.cs new file mode 100644 index 00000000..7b20a316 --- /dev/null +++ b/src/Transactions/CredentialDeployment.cs @@ -0,0 +1,36 @@ +using Concordium.Sdk.Exceptions; +using Concordium.Sdk.Types; +using CredentialDeploymentPayloadCase = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; + +namespace Concordium.Sdk.Transactions; + +/// +/// Credential deployments create new accounts. They are not paid for +/// directly by the sender. Instead, bakers are rewarded by the protocol for +/// including them. +/// +/// Latest time the credential deployment can included in a block. +/// The payload of the credential deployment. +public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload) : BlockItemType +{ + internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => + new( + TransactionTime.From(cred.MessageExpiry), + cred.PayloadCase switch + { + CredentialDeploymentPayloadCase.RawPayload => new CredentialPayloadRaw(cred.RawPayload.ToByteArray()), + CredentialDeploymentPayloadCase.None => throw new NotImplementedException(), + _ => throw new MissingEnumException(cred.PayloadCase), + } + ); +} + +/// The payload of a Credential Deployment. +public interface ICredentialPayload { }; + +/// +/// A raw payload, which is just the encoded payload. +/// A typed variant might be added in the future. +/// +public sealed record CredentialPayloadRaw(byte[] RawPayload) : ICredentialPayload; + diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index ae782231..3065f208 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -6,6 +6,27 @@ namespace Concordium.Sdk.Transactions; /// The smart contract module to be deployed. public sealed record DeployModule(VersionedModuleSource Module) : AccountTransactionPayload { + /// + /// Prepares the account transaction payload for signing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + public PreparedAccountTransaction Prepare( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry + ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + + /// + /// The transaction specific cost for submitting this type of + /// transaction to the chain. + /// + /// This should reflect the transaction-specific costs defined here: + /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs + /// + private readonly EnergyAmount transactionCost = new(Module.BytesLength / 10); + /// The account transaction type to be used in the serialized payload. private const byte TransactionType = (byte)Types.TransactionType.DeployModule; @@ -31,7 +52,8 @@ private static byte[] Serialize(VersionedModuleSource module) /// Where to write the result of the operation. public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, string? Error) output) { - if (bytes.Length <= 9) + var minLength = sizeof(TransactionType) + (2 * sizeof(int)); + if (bytes.Length <= minLength) { var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least 9, found {bytes.Length}"; output = (null, msg); @@ -56,8 +78,6 @@ public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, string? return true; } - public override ulong GetTransactionSpecificCost() => 300; - public override byte[] ToBytes() => Serialize(this.Module); } diff --git a/src/Transactions/PreparedAccountTransaction.cs b/src/Transactions/PreparedAccountTransaction.cs index 649e3bd2..db09556c 100644 --- a/src/Transactions/PreparedAccountTransaction.cs +++ b/src/Transactions/PreparedAccountTransaction.cs @@ -14,11 +14,13 @@ namespace Concordium.Sdk.Transactions; /// Address of the sender of the transaction. /// Account sequence number to use for the transaction. /// Expiration time of the transaction. +/// The maximum energy cost of this transaction. /// Payload to send to the node. public sealed record PreparedAccountTransaction( AccountAddress Sender, AccountSequenceNumber SequenceNumber, Expiry Expiry, + EnergyAmount Energy, AccountTransactionPayload Payload ) { @@ -34,10 +36,9 @@ public SignedAccountTransaction Sign(ITransactionSigner transactionSigner) var serializedPayloadSize = (uint)serializedPayload.Length; // Compute the energy cost. - var txSpecificCost = this.Payload.GetTransactionSpecificCost(); var energyCost = CalculateEnergyCost( transactionSigner.GetSignatureCount(), - txSpecificCost, + this.Energy.Value, AccountTransactionHeader.BytesLength, serializedPayloadSize ); diff --git a/src/Transactions/RawPayload.cs b/src/Transactions/RawPayload.cs new file mode 100644 index 00000000..e05e7dd3 --- /dev/null +++ b/src/Transactions/RawPayload.cs @@ -0,0 +1,14 @@ +namespace Concordium.Sdk.Transactions; + +/// +/// Represents the raw payload of an account transaction. +/// +/// Used mostly for debugging, the only place where this will be encountered +/// is when querying transactions from chain that have not been implemented for +/// this SDK yet, and thus can not be deserialized. +/// +/// The raw bytes of the payload. +public sealed record RawPayload(byte[] bytes) : AccountTransactionPayload +{ + public override byte[] ToBytes() => this.bytes; +} diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index c7a7ce4b..c2f8da41 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -15,7 +15,26 @@ public sealed record RegisterData(OnChainData Data) : AccountTransactionPayload /// private const byte TransactionType = (byte)Types.TransactionType.RegisterData; - public override ulong GetTransactionSpecificCost() => 300; + /// + /// Prepares the account transaction payload for signing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + public PreparedAccountTransaction Prepare( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry + ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + + /// + /// The transaction specific cost for submitting this type of + /// transaction to the chain. + /// + /// This should reflect the transaction-specific costs defined here: + /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs + /// + internal readonly EnergyAmount transactionCost = new(300); /// /// Copies the "register data" account transaction in the binary format expected by the node to a byte array. diff --git a/src/Transactions/SignedAccountTransaction.cs b/src/Transactions/SignedAccountTransaction.cs index fcded504..ea7397f4 100644 --- a/src/Transactions/SignedAccountTransaction.cs +++ b/src/Transactions/SignedAccountTransaction.cs @@ -1,5 +1,4 @@ using Concordium.Grpc.V2; -using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index dcbcba9d..9c8276c3 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -21,6 +21,27 @@ public sealed record Transfer(CcdAmount Amount, AccountAddress Receiver) : Accou /// private const uint BytesLength = sizeof(TransactionType) + AccountAddress.BytesLength + CcdAmount.BytesLength; + /// + /// Prepares the account transaction payload for signing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + public PreparedAccountTransaction Prepare( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry + ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + + /// + /// The transaction specific cost for submitting this type of + /// transaction to the chain. + /// + /// This should reflect the transaction-specific costs defined here: + /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs + /// + internal readonly EnergyAmount transactionCost = new(300); + /// /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. /// @@ -77,7 +98,5 @@ public static bool TryDeserial(byte[] bytes, out (Transfer?, string? Error) outp return true; } - public override ulong GetTransactionSpecificCost() => 300; - public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver); } diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 75129e18..6b582d16 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -18,6 +18,27 @@ public sealed record TransferWithMemo(CcdAmount Amount, AccountAddress Receiver, /// private const byte TransactionType = (byte)Types.TransactionType.TransferWithMemo; + /// + /// Prepares the account transaction payload for signing. + /// + /// Address of the sender of the transaction. + /// Account sequence number to use for the transaction. + /// Expiration time of the transaction. + public PreparedAccountTransaction Prepare( + AccountAddress sender, + AccountSequenceNumber sequenceNumber, + Expiry expiry + ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + + /// + /// The transaction specific cost for submitting this type of + /// transaction to the chain. + /// + /// This should reflect the transaction-specific costs defined here: + /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs + /// + internal readonly EnergyAmount transactionCost = new(300); + /// /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. /// @@ -91,7 +112,5 @@ public static bool TryDeserial(byte[] bytes, out (TransferWithMemo?, string? Err return true; } - public override ulong GetTransactionSpecificCost() => 300; - public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver, this.Memo); } diff --git a/src/Types/BlockItem.cs b/src/Transactions/UpdateInstruction.cs similarity index 51% rename from src/Types/BlockItem.cs rename to src/Transactions/UpdateInstruction.cs index f3ba33c7..6e61c24b 100644 --- a/src/Types/BlockItem.cs +++ b/src/Transactions/UpdateInstruction.cs @@ -1,10 +1,8 @@ using Concordium.Sdk.Exceptions; -using Concordium.Sdk.Transactions; -using BlockItemCase = Concordium.Grpc.V2.BlockItem.BlockItemOneofCase; -using CredentialDeploymentPayloadCase = Concordium.Grpc.V2.CredentialDeployment.PayloadOneofCase; +using Concordium.Sdk.Types; using UpdateInstructionPayloadCase = Concordium.Grpc.V2.UpdateInstructionPayload.PayloadOneofCase; -namespace Concordium.Sdk.Types; +namespace Concordium.Sdk.Transactions; /// /// Update instructions are messages which can update the chain parameters. Including which keys are allowed @@ -66,55 +64,3 @@ public interface IUpdateInstructionPayload { } /// A raw payload encoded according to the format defined by the protocol. public sealed record UpdateInstructionPayloadRaw(byte[] RawPayload) : IUpdateInstructionPayload; - -/// -/// Credential deployments create new accounts. They are not paid for -/// directly by the sender. Instead, bakers are rewarded by the protocol for -/// including them. -/// -/// Latest time the credential deployment can included in a block. -/// The payload of the credential deployment. -public record CredentialDeployment(TransactionTime MessageExpiry, ICredentialPayload Payload) : BlockItemType -{ - internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => - new( - TransactionTime.From(cred.MessageExpiry), - cred.PayloadCase switch - { - CredentialDeploymentPayloadCase.RawPayload => new CredentialPayloadRaw(cred.RawPayload.ToByteArray()), - CredentialDeploymentPayloadCase.None => throw new NotImplementedException(), - _ => throw new MissingEnumException(cred.PayloadCase), - } - ); -} - -/// The payload of a Credential Deployment. -public interface ICredentialPayload { }; - -/// -/// A raw payload, which is just the encoded payload. -/// A typed variant might be added in the future. -/// -public sealed record CredentialPayloadRaw(byte[] RawPayload) : ICredentialPayload; - -/// A block item. -/// The hash of the block item that identifies it to the chain. -/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. -public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType BlockItemType) -{ - internal static BlockItem From(Grpc.V2.BlockItem blockItem) => - new( - TransactionHash.From(blockItem.Hash.ToString()), - blockItem.BlockItemCase switch - { - BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), - BlockItemCase.CredentialDeployment => CredentialDeployment.From(blockItem.CredentialDeployment), - BlockItemCase.UpdateInstruction => UpdateInstruction.From(blockItem.UpdateInstruction), - BlockItemCase.None => throw new NotImplementedException(), - _ => throw new MissingEnumException(blockItem.BlockItemCase), - } - ); -} - -/// Either a SignedAccountTransaction, CredentialDeployment or UpdateInstruction. -public abstract record BlockItemType; diff --git a/tests/IntegrationTests/Tests.cs b/tests/IntegrationTests/Tests.cs index cf3036fb..7474757b 100644 --- a/tests/IntegrationTests/Tests.cs +++ b/tests/IntegrationTests/Tests.cs @@ -54,9 +54,19 @@ protected async Task AwaitFinalization(TransactionHa protected async Task Transfer(ITransactionSigner account, AccountAddress sender, AccountTransactionPayload transactionPayload, CancellationToken token) { - var (accountSequenceNumber, _) = await this.Client.GetNextAccountSequenceNumberAsync(sender, token); - var preparedAccountTransaction = transactionPayload.Prepare(sender, accountSequenceNumber, Expiry.AtMinutesFromNow(30)); - var signedTransfer = preparedAccountTransaction.Sign(account); + var (sequenceNumber, _) = await this.Client.GetNextAccountSequenceNumberAsync(sender, token); + var expiry = Expiry.AtMinutesFromNow(30); + + var prepared = transactionPayload switch + { + Transfer transfer => transfer.Prepare(sender, sequenceNumber, expiry), + TransferWithMemo transferWithMemo => transferWithMemo.Prepare(sender, sequenceNumber, expiry), + DeployModule deployModule => deployModule.Prepare(sender, sequenceNumber, expiry), + RegisterData registerData => registerData.Prepare(sender, sequenceNumber, expiry), + _ => throw new NotImplementedException(), + }; + + var signedTransfer = prepared.Sign(account); var txHash = await this.Client.SendAccountTransactionAsync(signedTransfer, token); return txHash; } diff --git a/tests/UnitTests/Transactions/TransactionTestHelpers.cs b/tests/UnitTests/Transactions/TransactionTestHelpers.cs index e69f5c92..1271b9c9 100644 --- a/tests/UnitTests/Transactions/TransactionTestHelpers.cs +++ b/tests/UnitTests/Transactions/TransactionTestHelpers.cs @@ -21,7 +21,15 @@ AccountTransactionPayload transaction var sender = AccountAddress.From("3QuZ47NkUk5icdDSvnfX8HiJzCnSRjzi6KwGEmqgQ7hCXNBTWN"); var sequenceNumber = AccountSequenceNumber.From(123); var expiry = Expiry.From(65537); - return transaction.Prepare(sender, sequenceNumber, expiry); + + return transaction switch + { + Transfer transfer => transfer.Prepare(sender, sequenceNumber, expiry), + TransferWithMemo transferWithMemo => transferWithMemo.Prepare(sender, sequenceNumber, expiry), + DeployModule deployModule => deployModule.Prepare(sender, sequenceNumber, expiry), + RegisterData registerData => registerData.Prepare(sender, sequenceNumber, expiry), + _ => throw new System.NotImplementedException(), + }; } /// From 30838ed9ed5484c48bc178154608e18c041b0fde Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 28 Nov 2023 10:03:33 +0100 Subject: [PATCH 10/21] Self-review --- .../GetBlockPendingUpdates/GetBlocks.csproj | 18 --------------- src/Exceptions/DeserialException.cs | 2 +- src/Helpers/Deserialization.cs | 14 +----------- src/Transactions/AccountTransactionHeader.cs | 2 +- src/Transactions/AccountTransactionPayload.cs | 22 ------------------- src/Transactions/TransferWithMemo.cs | 6 ++++- src/Types/AccountAddress.cs | 7 ++++-- src/Types/CcdAmount.cs | 4 ++-- src/Types/OnChainData.cs | 4 ++-- src/Types/VersionedModuleSource.cs | 7 +++++- 10 files changed, 23 insertions(+), 63 deletions(-) delete mode 100644 examples/GetBlockPendingUpdates/GetBlocks.csproj diff --git a/examples/GetBlockPendingUpdates/GetBlocks.csproj b/examples/GetBlockPendingUpdates/GetBlocks.csproj deleted file mode 100644 index f9d1b111..00000000 --- a/examples/GetBlockPendingUpdates/GetBlocks.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - net6.0 - enable - enable - - - - - - - - - - - diff --git a/src/Exceptions/DeserialException.cs b/src/Exceptions/DeserialException.cs index 9813ce7a..c5cef779 100644 --- a/src/Exceptions/DeserialException.cs +++ b/src/Exceptions/DeserialException.cs @@ -1,7 +1,7 @@ namespace Concordium.Sdk.Exceptions; /// -/// Thrown when a matched enum value could not be handled in a switch statement. +/// Thrown when deserialization fails and is explicitly meant not to. /// public sealed class DeserialException : Exception { diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs index e351991a..7c89cce8 100644 --- a/src/Helpers/Deserialization.cs +++ b/src/Helpers/Deserialization.cs @@ -48,7 +48,7 @@ public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, str } /// - /// Creates a uint from a byte array. + /// Creates a ulong from a byte array. /// public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, string? Error) output) { @@ -66,18 +66,6 @@ public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, s output = (BinaryPrimitives.ReadUInt64BigEndian(bytes), null); return true; } - - // TODO: Debug tool remove - public static void PrintBytes(string msg, byte[] bytes) - { - Console.WriteLine(msg); - foreach (var b in bytes) - { - Console.Write(b); - Console.Write(" "); - } - Console.Write("\n"); - } } diff --git a/src/Transactions/AccountTransactionHeader.cs b/src/Transactions/AccountTransactionHeader.cs index 4ca90f5b..43777492 100644 --- a/src/Transactions/AccountTransactionHeader.cs +++ b/src/Transactions/AccountTransactionHeader.cs @@ -89,7 +89,7 @@ public Grpc.V2.AccountTransactionHeader ToProto() => }; /// - /// Converts the account transaction header to its corresponding protocol buffer message instance. + /// Creates an account transaction header from its corresponding protocol buffer message instance. /// internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader) => new( AccountAddress.From(accountTransactionHeader.Sender), diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index de1bbb07..8eb8a785 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -123,27 +123,5 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr } return parsedPayload.Item1; } - - /// - /// Prepares the account transaction payload for signing. Will throw an - /// exception if AccountTransaction is of subtype RawPayload. Should only - /// be used for testing. - /// - /// Address of the sender of the transaction. - /// Account sequence number to use for the transaction. - /// Expiration time of the transaction. - internal PreparedAccountTransaction PrepareWithException( - AccountAddress sender, - AccountSequenceNumber sequenceNumber, - Expiry expiry - ) => this switch - { - Transfer transfer => transfer.Prepare(sender, sequenceNumber, expiry), - TransferWithMemo transferWithMemo => transferWithMemo.Prepare(sender, sequenceNumber, expiry), - DeployModule deployModule => deployModule.Prepare(sender, sequenceNumber, expiry), - RegisterData registerData => registerData.Prepare(sender, sequenceNumber, expiry), - _ => throw new NotImplementedException(), - }; - } diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 6b582d16..65a534f2 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -47,7 +47,11 @@ Expiry expiry /// Memo to include with the transaction. private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnChainData memo) { - using var memoryStream = new MemoryStream((int)(sizeof(TransactionType) + CcdAmount.BytesLength + AccountAddress.BytesLength + OnChainData.MaxLength)); + using var memoryStream = new MemoryStream((int)( + sizeof(TransactionType) + + CcdAmount.BytesLength + + AccountAddress.BytesLength + + OnChainData.MaxLength)); memoryStream.WriteByte(TransactionType); memoryStream.Write(receiver.ToBytes()); memoryStream.Write(memo.ToBytes()); diff --git a/src/Types/AccountAddress.cs b/src/Types/AccountAddress.cs index bc5dc4e0..a07f199f 100644 --- a/src/Types/AccountAddress.cs +++ b/src/Types/AccountAddress.cs @@ -11,6 +11,9 @@ namespace Concordium.Sdk.Types; /// public sealed record AccountAddress : IEquatable, IAddress, IAccountIdentifier { + /// + /// The serialized length of the account address. + /// public const uint BytesLength = 32; /// @@ -217,9 +220,9 @@ public Grpc.V2.AccountIdentifierInput ToAccountIdentifierInput() => new() { Address = this.ToProto() }; /// - /// Create an account address from a serialized as bytes. + /// Create an account address from a byte array. /// - /// The account address as bytes. + /// The serialized account address. /// Where to write the result of the operation. public static bool TryDeserial(byte[] bytes, out (AccountAddress? accountAddress, string? Error) output) { diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs index 166a4357..6b4943bd 100644 --- a/src/Types/CcdAmount.cs +++ b/src/Types/CcdAmount.cs @@ -111,9 +111,9 @@ public static CcdAmount FromCcd(ulong ccd) } /// - /// Create a CCD amount from a serialized as bytes. + /// Create a CCD amount from a byte array. /// - /// The CCD amount as bytes. + /// The serialized CCD amount. /// Where to write the result of the operation. public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress, string? Error) output) { diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index 87d1946b..eb2f6fbd 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -136,9 +136,9 @@ public byte[] ToBytes() public override string ToString() => Convert.ToHexString(this._value).ToLowerInvariant(); /// - /// Create an account address from a serialized as bytes. + /// Create an "OnChainData" from a byte array. /// - /// The account address as bytes. + /// The serialized "OnChainData". /// Where to write the result of the operation. public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress, string? Error) output) { diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 7430c8f1..67a86337 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -48,7 +48,12 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version .ModuleCase) }; - internal static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) + /// + /// Create a versioned module schema from a byte array. + /// + /// The serialized schema. + /// Where to write the result of the operation. + public static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) { var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out var version); From b6592295da0cd7a52a89ee5753c0c439ef4145ad Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 28 Nov 2023 10:53:08 +0100 Subject: [PATCH 11/21] Pleased formatter --- src/Transactions/AccountTransactionPayload.cs | 35 ------------------- src/Transactions/DeployModule.cs | 4 +-- src/Transactions/RawPayload.cs | 6 ++-- src/Transactions/RegisterData.cs | 4 +-- src/Transactions/Transfer.cs | 4 +-- src/Transactions/TransferWithMemo.cs | 4 +-- src/Types/CcdAmount.cs | 4 +-- src/Types/VersionedModuleSource.cs | 6 ++-- 8 files changed, 17 insertions(+), 50 deletions(-) diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 8eb8a785..73fc608c 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -78,41 +78,6 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr parsedPayload = output; break; } - - case TransactionType.InitContract: - break; - case TransactionType.Update: - break; - case TransactionType.AddBaker: - break; - case TransactionType.RemoveBaker: - break; - case TransactionType.UpdateBakerStake: - break; - case TransactionType.UpdateBakerRestakeEarnings: - break; - case TransactionType.UpdateBakerKeys: - break; - case TransactionType.UpdateCredentialKeys: - break; - case TransactionType.EncryptedAmountTransfer: - break; - case TransactionType.TransferToEncrypted: - break; - case TransactionType.TransferToPublic: - break; - case TransactionType.TransferWithSchedule: - break; - case TransactionType.UpdateCredentials: - break; - case TransactionType.EncryptedAmountTransferWithMemo: - break; - case TransactionType.TransferWithScheduleAndMemo: - break; - case TransactionType.ConfigureBaker: - break; - case TransactionType.ConfigureDelegation: - break; default: throw new NotImplementedException(); }; diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 3065f208..3865cb1a 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -16,7 +16,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); /// /// The transaction specific cost for submitting this type of @@ -25,7 +25,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount transactionCost = new(Module.BytesLength / 10); + private readonly EnergyAmount _transactionCost = new(Module.BytesLength / 10); /// The account transaction type to be used in the serialized payload. private const byte TransactionType = (byte)Types.TransactionType.DeployModule; diff --git a/src/Transactions/RawPayload.cs b/src/Transactions/RawPayload.cs index e05e7dd3..8277209f 100644 --- a/src/Transactions/RawPayload.cs +++ b/src/Transactions/RawPayload.cs @@ -7,8 +7,8 @@ namespace Concordium.Sdk.Transactions; /// is when querying transactions from chain that have not been implemented for /// this SDK yet, and thus can not be deserialized. /// -/// The raw bytes of the payload. -public sealed record RawPayload(byte[] bytes) : AccountTransactionPayload +/// The raw bytes of the payload. +public sealed record RawPayload(byte[] Bytes) : AccountTransactionPayload { - public override byte[] ToBytes() => this.bytes; + public override byte[] ToBytes() => this.Bytes; } diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index c2f8da41..053bb502 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -25,7 +25,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); /// /// The transaction specific cost for submitting this type of @@ -34,7 +34,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - internal readonly EnergyAmount transactionCost = new(300); + private readonly EnergyAmount _transactionCost = new(300); /// /// Copies the "register data" account transaction in the binary format expected by the node to a byte array. diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index 9c8276c3..0d4aabad 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -31,7 +31,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); /// /// The transaction specific cost for submitting this type of @@ -40,7 +40,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - internal readonly EnergyAmount transactionCost = new(300); + private readonly EnergyAmount _transactionCost = new(300); /// /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 65a534f2..5ff8d504 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -28,7 +28,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this.transactionCost, this); + ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); /// /// The transaction specific cost for submitting this type of @@ -37,7 +37,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - internal readonly EnergyAmount transactionCost = new(300); + private readonly EnergyAmount _transactionCost = new(300); /// /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs index 6b4943bd..458befa5 100644 --- a/src/Types/CcdAmount.cs +++ b/src/Types/CcdAmount.cs @@ -125,9 +125,9 @@ public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress, str }; // This call also verifies the length - var U64Deserial = Deserial.TryDeserialU64(bytes, 0, out var amount); + var u64Deserial = Deserial.TryDeserialU64(bytes, 0, out var amount); - if (!U64Deserial) + if (!u64Deserial) { output = (null, amount.Error); return false; diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 67a86337..037f970a 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -9,13 +9,15 @@ namespace Concordium.Sdk.Types; public abstract record VersionedModuleSource(byte[] Source) : IEquatable { internal const uint MaxLength = 8 * 65536; - internal uint BytesLength = (2 * sizeof(int)) + (uint)Source.Length; internal abstract uint GetVersion(); internal byte[] ToBytes() { - using var memoryStream = new MemoryStream((int)this.BytesLength); + using var memoryStream = new MemoryStream( + (2 * sizeof(int)) + + this.Source.Length + ); memoryStream.Write(Serialization.ToBytes(this.GetVersion())); memoryStream.Write(Serialization.ToBytes((uint)this.Source.Length)); memoryStream.Write(this.Source); From 193d694072b6495edb4871d42e09fc096c4be0c6 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 28 Nov 2023 10:55:50 +0100 Subject: [PATCH 12/21] Added changelog --- CHANGELOG.md | 4 +- examples/GetBlockItems/GetBlockItems.csproj | 18 +++++++++ examples/GetBlockItems/Program.cs | 38 +++++++++++++++++++ src/Client/ConcordiumClient.cs | 17 +++++++++ src/Transactions/AccountTransactionPayload.cs | 35 +++++++++++++++++ src/Transactions/DeployModule.cs | 5 ++- src/Types/VersionedModuleSource.cs | 7 ++-- 7 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 examples/GetBlockItems/GetBlockItems.csproj create mode 100644 examples/GetBlockItems/Program.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index de72687b..44c241fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Unreleased changes - Added - - New GRPC-endpoints: `GetBlocks`, `GetFinalizedBlocks`, `GetBranches`, `GetAncestors`, `GetBlockPendingUpdates` + - New GRPC-endpoints: `GetBlocks`, `GetFinalizedBlocks`, `GetBranches`, `GetAncestors`, `GetBlockPendingUpdates`, `GetBlockItems` + - New transaction `DeployModule` + - Added serialization and deserialization for all instances of `AccountTransactionPayload` ## 4.1.0 - Bugfix diff --git a/examples/GetBlockItems/GetBlockItems.csproj b/examples/GetBlockItems/GetBlockItems.csproj new file mode 100644 index 00000000..f9d1b111 --- /dev/null +++ b/examples/GetBlockItems/GetBlockItems.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/examples/GetBlockItems/Program.cs b/examples/GetBlockItems/Program.cs new file mode 100644 index 00000000..05da0a0b --- /dev/null +++ b/examples/GetBlockItems/Program.cs @@ -0,0 +1,38 @@ +using CommandLine; +using Concordium.Sdk.Client; + +// We disable these warnings since CommandLine needs to set properties in options +// but we don't want to give default values. +#pragma warning disable CS8618 + +namespace GetBlockItems; + +internal sealed class GetBlocksOptions +{ + [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", + Default = "http://node.testnet.concordium.com:20000/")] + public string Endpoint { get; set; } +} + +public static class Program +{ + /// + /// Example how to use + /// + public static async Task Main(string[] args) => + await Parser.Default + .ParseArguments(args) + .WithParsedAsync(Run); + + private static async Task Run(GetBlocksOptions options) + { + using var client = new ConcordiumClient(new Uri(options.Endpoint), new ConcordiumClientOptions()); + + var blockItems = client.GetBlockItems(); + + await foreach (var item in blockItems) + { + Console.WriteLine($"Blockitem: {item}"); + } + } +} diff --git a/src/Client/ConcordiumClient.cs b/src/Client/ConcordiumClient.cs index 6d938b8f..6a62ffc3 100644 --- a/src/Client/ConcordiumClient.cs +++ b/src/Client/ConcordiumClient.cs @@ -9,6 +9,7 @@ using BakerId = Concordium.Sdk.Types.BakerId; using BlockHash = Concordium.Sdk.Types.BlockHash; using BlockInfo = Concordium.Sdk.Types.BlockInfo; +using BlockItem = Concordium.Sdk.Transactions.BlockItem; using BlockItemSummary = Concordium.Sdk.Types.BlockItemSummary; using Branch = Concordium.Sdk.Types.Branch; using ConsensusInfo = Concordium.Sdk.Types.ConsensusInfo; @@ -742,5 +743,21 @@ await Task.WhenAll(response.ResponseHeadersAsync, response.ResponseAsync) .ConfigureAwait(false); } + /// + /// Get the items of a block. + /// + /// Block hash from where smart contract information will be given. + /// Cancellation token + /// A stream of block items. + /// + /// RPC error occurred, access for more information. + /// indicates that this endpoint is disabled in the node. + /// + public IAsyncEnumerable GetBlockItems(IBlockHashInput blockHashInput, CancellationToken token = default) + { + var response = this.Raw.GetBlockItems(blockHashInput.Into(), token); + return response.ResponseStream.ReadAllAsync(token).Select(BlockItem.From); + } + public void Dispose() => this.Raw.Dispose(); } diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 73fc608c..8eb8a785 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -78,6 +78,41 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr parsedPayload = output; break; } + + case TransactionType.InitContract: + break; + case TransactionType.Update: + break; + case TransactionType.AddBaker: + break; + case TransactionType.RemoveBaker: + break; + case TransactionType.UpdateBakerStake: + break; + case TransactionType.UpdateBakerRestakeEarnings: + break; + case TransactionType.UpdateBakerKeys: + break; + case TransactionType.UpdateCredentialKeys: + break; + case TransactionType.EncryptedAmountTransfer: + break; + case TransactionType.TransferToEncrypted: + break; + case TransactionType.TransferToPublic: + break; + case TransactionType.TransferWithSchedule: + break; + case TransactionType.UpdateCredentials: + break; + case TransactionType.EncryptedAmountTransferWithMemo: + break; + case TransactionType.TransferWithScheduleAndMemo: + break; + case TransactionType.ConfigureBaker: + break; + case TransactionType.ConfigureDelegation: + break; default: throw new NotImplementedException(); }; diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 3865cb1a..951c94e1 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -25,7 +25,8 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(Module.BytesLength / 10); + + private readonly EnergyAmount _transactionCost = new(Module.GetBytesLength() / 10); /// The account transaction type to be used in the serialized payload. private const byte TransactionType = (byte)Types.TransactionType.DeployModule; @@ -38,7 +39,7 @@ private static byte[] Serialize(VersionedModuleSource module) { using var memoryStream = new MemoryStream((int)( sizeof(TransactionType) + - module.BytesLength + module.GetBytesLength() )); memoryStream.WriteByte(TransactionType); memoryStream.Write(module.ToBytes()); diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 037f970a..1c9de172 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -12,12 +12,11 @@ public abstract record VersionedModuleSource(byte[] Source) : IEquatable (uint)((2 * sizeof(int)) + this.Source.Length); + internal byte[] ToBytes() { - using var memoryStream = new MemoryStream( - (2 * sizeof(int)) + - this.Source.Length - ); + using var memoryStream = new MemoryStream((int)this.GetBytesLength()); memoryStream.Write(Serialization.ToBytes(this.GetVersion())); memoryStream.Write(Serialization.ToBytes((uint)this.Source.Length)); memoryStream.Write(this.Source); From 6b773df0c06671ebce2dd54a822330b8ff843344 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 28 Nov 2023 13:49:18 +0100 Subject: [PATCH 13/21] Fixed example --- examples/GetBlockItems/Program.cs | 18 ++++++++++++++---- src/Transactions/BlockItem.cs | 2 +- src/Types/TransactionHash.cs | 3 +++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/GetBlockItems/Program.cs b/examples/GetBlockItems/Program.cs index 05da0a0b..898ee79c 100644 --- a/examples/GetBlockItems/Program.cs +++ b/examples/GetBlockItems/Program.cs @@ -1,5 +1,6 @@ using CommandLine; using Concordium.Sdk.Client; +using Concordium.Sdk.Types; // We disable these warnings since CommandLine needs to set properties in options // but we don't want to give default values. @@ -12,23 +13,31 @@ internal sealed class GetBlocksOptions [Option(HelpText = "URL representing the endpoint where the gRPC V2 API is served.", Default = "http://node.testnet.concordium.com:20000/")] public string Endpoint { get; set; } + [Option( + 'b', + "block-hash", + HelpText = "Block hash of the block. Defaults to LastFinal." + )] + public string BlockHash { get; set; } } public static class Program { /// - /// Example how to use + /// Example how to use /// public static async Task Main(string[] args) => await Parser.Default .ParseArguments(args) .WithParsedAsync(Run); - private static async Task Run(GetBlocksOptions options) + private static async Task Run(GetBlocksOptions o) { - using var client = new ConcordiumClient(new Uri(options.Endpoint), new ConcordiumClientOptions()); + using var client = new ConcordiumClient(new Uri(o.Endpoint), new ConcordiumClientOptions()); - var blockItems = client.GetBlockItems(); + IBlockHashInput bi = o.BlockHash != null ? new Given(BlockHash.From(o.BlockHash)) : new LastFinal(); + + var blockItems = client.GetBlockItems(bi); await foreach (var item in blockItems) { @@ -36,3 +45,4 @@ private static async Task Run(GetBlocksOptions options) } } } + diff --git a/src/Transactions/BlockItem.cs b/src/Transactions/BlockItem.cs index ec2a0a13..69dfe4c7 100644 --- a/src/Transactions/BlockItem.cs +++ b/src/Transactions/BlockItem.cs @@ -11,7 +11,7 @@ public sealed record BlockItem(TransactionHash TransactionHash, BlockItemType Bl { internal static BlockItem From(Grpc.V2.BlockItem blockItem) => new( - TransactionHash.From(blockItem.Hash.ToString()), + TransactionHash.From(blockItem.Hash), blockItem.BlockItemCase switch { BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), diff --git a/src/Types/TransactionHash.cs b/src/Types/TransactionHash.cs index 469ebc32..0fd76798 100644 --- a/src/Types/TransactionHash.cs +++ b/src/Types/TransactionHash.cs @@ -25,6 +25,9 @@ public static TransactionHash From(string transactionHashAsBase16String) => public static TransactionHash From(byte[] transactionHashAsBytes) => new(transactionHashAsBytes); + internal static TransactionHash From(Grpc.V2.TransactionHash proto) => + new(proto.Value.ToArray()); + /// /// Converts the transaction hash to its corresponding protocol buffer message instance. /// From 2e8fd9a580287e14d2f6555224254209b76e28b0 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 5 Dec 2023 14:56:26 +0100 Subject: [PATCH 14/21] Addressed some comments --- src/Client/ConcordiumClient.cs | 6 +- src/Helpers/Deserialization.cs | 70 ------------------- src/Transactions/AccountTransactionPayload.cs | 21 +----- src/Transactions/DeployModule.cs | 19 +++-- src/Transactions/RegisterData.cs | 17 +++-- src/Transactions/Transfer.cs | 23 +++--- src/Transactions/TransferWithMemo.cs | 35 +++++----- src/Types/AccountAddress.cs | 8 +-- src/Types/CcdAmount.cs | 18 ++--- src/Types/OnChainData.cs | 24 +++---- src/Types/VersionedModuleSource.cs | 25 +++---- tests/UnitTests/Transactions/DeployModule.cs | 10 ++- .../Transactions/RegisterDataTests.cs | 10 ++- tests/UnitTests/Transactions/TransferTests.cs | 13 ++-- .../Transactions/TransferWithMemoTests.cs | 10 ++- 15 files changed, 122 insertions(+), 187 deletions(-) diff --git a/src/Client/ConcordiumClient.cs b/src/Client/ConcordiumClient.cs index 67f505ad..dfd249f7 100644 --- a/src/Client/ConcordiumClient.cs +++ b/src/Client/ConcordiumClient.cs @@ -746,17 +746,17 @@ await Task.WhenAll(response.ResponseHeadersAsync, response.ResponseAsync) /// /// Get the items of a block. /// - /// Block hash from where smart contract information will be given. + /// Identifies what block to get the information from. /// Cancellation token /// A stream of block items. /// /// RPC error occurred, access for more information. /// indicates that this endpoint is disabled in the node. /// - public IAsyncEnumerable GetBlockItems(IBlockHashInput blockHashInput, CancellationToken token = default) + public Task>> GetBlockItems(IBlockHashInput blockHashInput, CancellationToken token = default) { var response = this.Raw.GetBlockItems(blockHashInput.Into(), token); - return response.ResponseStream.ReadAllAsync(token).Select(BlockItem.From); + return QueryResponse>.From(response, BlockItem.From, token); } public void Dispose() => this.Raw.Dispose(); diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs index 7c89cce8..8b137891 100644 --- a/src/Helpers/Deserialization.cs +++ b/src/Helpers/Deserialization.cs @@ -1,71 +1 @@ -using System.Buffers.Binary; - -namespace Concordium.Sdk.Helpers; - -/// -/// Helpers for deserializing data. -/// -public static class Deserial -{ - /// - /// Creates a ushort from a byte array. - /// - public static bool TryDeserialU16(byte[] input, int offset, out (ushort? Uint, string? Error) output) - { - if (input.Length < sizeof(ushort)) - { - var msg = $"Invalid length in TryDeserialU32. Must be longer than {sizeof(ushort)}, but was {input.Length}"; - output = (null, msg); - return false; - } - - var offset_input = input.Skip(offset).ToArray(); - - var bytes = offset_input.Take(sizeof(ushort)).ToArray(); - - output = (BinaryPrimitives.ReadUInt16BigEndian(bytes), null); - return true; - } - - /// - /// Creates a uint from a byte array. - /// - public static bool TryDeserialU32(byte[] input, int offset, out (uint? Uint, string? Error) output) - { - if (input.Length < sizeof(uint)) - { - var msg = $"Invalid length in TryDeserialU32. Must be longer than 4, but was {input.Length}"; - output = (null, msg); - return false; - } - - var offset_input = input.Skip(offset).ToArray(); - - var bytes = offset_input.Take(sizeof(uint)).ToArray(); - - output = (BinaryPrimitives.ReadUInt32BigEndian(bytes), null); - return true; - } - - /// - /// Creates a ulong from a byte array. - /// - public static bool TryDeserialU64(byte[] input, int offset, out (ulong? Ulong, string? Error) output) - { - if (input.Length < sizeof(ulong)) - { - var msg = $"Invalid length in TryDeserialU32. Must be longer than {sizeof(ulong)}, but was {input.Length}"; - output = (null, msg); - return false; - } - - var offset_input = input.Skip(offset).ToArray(); - - var bytes = offset_input.Take(sizeof(ulong)).ToArray(); - - output = (BinaryPrimitives.ReadUInt64BigEndian(bytes), null); - return true; - } -} - diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 8eb8a785..366b21cf 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -78,43 +78,26 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr parsedPayload = output; break; } - case TransactionType.InitContract: - break; case TransactionType.Update: - break; case TransactionType.AddBaker: - break; case TransactionType.RemoveBaker: - break; case TransactionType.UpdateBakerStake: - break; case TransactionType.UpdateBakerRestakeEarnings: - break; case TransactionType.UpdateBakerKeys: - break; case TransactionType.UpdateCredentialKeys: - break; case TransactionType.EncryptedAmountTransfer: - break; case TransactionType.TransferToEncrypted: - break; case TransactionType.TransferToPublic: - break; case TransactionType.TransferWithSchedule: - break; case TransactionType.UpdateCredentials: - break; case TransactionType.EncryptedAmountTransferWithMemo: - break; case TransactionType.TransferWithScheduleAndMemo: - break; case TransactionType.ConfigureBaker: - break; case TransactionType.ConfigureDelegation: - break; default: - throw new NotImplementedException(); + parsedPayload = (new RawPayload(payload.ToArray()), null); + break; }; if (parsedPayload.Item2 != null) diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 951c94e1..3a55951c 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -51,26 +51,33 @@ private static byte[] Serialize(VersionedModuleSource module) /// /// The "deploy module" payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (DeployModule? Module, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (DeployModule? Module, string? Error) output) { var minLength = sizeof(TransactionType) + (2 * sizeof(int)); - if (bytes.Length <= minLength) + if (bytes.Length < minLength) { - var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least 9, found {bytes.Length}"; + var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least {minLength}, found {bytes.Length}"; output = (null, msg); return false; } - var deserialSuccess = VersionedModuleSourceFactory.TryDeserial(bytes.Skip(1).ToArray(), out var module); + if (bytes[0] != TransactionType) + { + var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + output = (null, msg); + return false; + } + var deserialSuccess = VersionedModuleSourceFactory.TryDeserial(bytes[sizeof(TransactionType)..], out var module); if (!deserialSuccess) { output = (null, module.Error); return false; }; - if (bytes[0] != TransactionType) + + if (module.VersionedModuleSource == null) { - var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + var msg = $"The parsed output is null, but no error was found. This should not be possible."; output = (null, msg); return false; } diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index 053bb502..2c31a061 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -55,7 +55,7 @@ private static byte[] Serialize(OnChainData data) /// /// The payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (RegisterData?, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (RegisterData? RegisterData, string? Error) output) { var minSize = sizeof(TransactionType); if (bytes.Length < minSize) @@ -71,16 +71,21 @@ public static bool TryDeserial(byte[] bytes, out (RegisterData?, string? Error) return false; }; - var memoBytes = bytes.Skip(sizeof(TransactionType)).ToArray(); - var memoDeserial = OnChainData.TryDeserial(memoBytes, out var memo); - - if (!memoDeserial) + var memoBytes = bytes[sizeof(TransactionType)..]; + if (!OnChainData.TryDeserial(memoBytes, out var memo)) { output = (null, memo.Error); return false; }; - output = (new RegisterData(memo.accountAddress), null); + if (memo.OnChainData == null) + { + var msg = $"The parsed output is null, but no error was found. This should not be possible."; + output = (null, msg); + return false; + }; + + output = (new RegisterData(memo.OnChainData), null); return true; } diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index 0d4aabad..f478e253 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -61,7 +61,7 @@ private static byte[] Serialize(CcdAmount amount, AccountAddress receiver) /// /// The "transfer" payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (Transfer?, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (Transfer? Transfer, string? Error) output) { if (bytes.Length != BytesLength) { @@ -76,25 +76,28 @@ public static bool TryDeserial(byte[] bytes, out (Transfer?, string? Error) outp return false; }; - var accountBytes = bytes.Skip(1).Take((int)AccountAddress.BytesLength).ToArray(); - var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); - - if (!accDeserial) + var accountBytes = bytes[sizeof(TransactionType)..]; + if (!AccountAddress.TryDeserial(accountBytes, out var account)) { output = (null, account.Error); return false; }; - var amountBytes = bytes.Skip((int)AccountAddress.BytesLength + 1).ToArray(); - var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); - - if (!amountDeserial) + var amountBytes = bytes[((int)AccountAddress.BytesLength + sizeof(TransactionType))..]; + if (!CcdAmount.TryDeserial(amountBytes, out var amount)) { output = (null, amount.Error); return false; }; - output = (new Transfer(amount.accountAddress.Value, account.accountAddress), null); + if (amount.Amount == null || account.AccountAddress == null) + { + var msg = $"The parsed output is null, but no error was found. This should not be possible."; + output = (null, msg); + return false; + } + + output = (new Transfer(amount.Amount.Value, account.AccountAddress), null); return true; } diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 5ff8d504..e0164aa7 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -64,9 +65,9 @@ private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnCha /// /// The payload as bytes. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (TransferWithMemo?, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (TransferWithMemo? Transfer, string? Error) output) { - var minSize = sizeof(TransactionType) + AccountAddress.BytesLength + CcdAmount.BytesLength; + var minSize = sizeof(TransactionType) + AccountAddress.BytesLength + CcdAmount.BytesLength + sizeof(ushort); if (bytes.Length < minSize) { var msg = $"Invalid length in `TransferWithMemo.TryDeserial`. Expected at least {minSize}, found {bytes.Length}"; @@ -82,37 +83,37 @@ public static bool TryDeserial(byte[] bytes, out (TransferWithMemo?, string? Err var trxTypeLength = sizeof(TransactionType); var accountLength = (int)AccountAddress.BytesLength; - var amountLength = (int)CcdAmount.BytesLength; - var memoLength = bytes.Length - trxTypeLength - accountLength - amountLength; + var memoLength = BinaryPrimitives.ReadUInt16BigEndian(bytes[(trxTypeLength + accountLength)..]) + sizeof(ushort); - var accountBytes = bytes.Skip(trxTypeLength).Take(accountLength).ToArray(); - var accDeserial = AccountAddress.TryDeserial(accountBytes, out var account); - - if (!accDeserial) + var accountBytes = bytes[trxTypeLength..]; + if (!AccountAddress.TryDeserial(accountBytes, out var account)) { output = (null, account.Error); return false; }; - var memoBytes = bytes.Skip(trxTypeLength + accountLength).Take(memoLength).ToArray(); - var memoDeserial = OnChainData.TryDeserial(memoBytes, out var memo); - - if (!memoDeserial) + var memoBytes = bytes[(trxTypeLength + accountLength)..]; + if (!OnChainData.TryDeserial(memoBytes, out var memo)) { output = (null, memo.Error); return false; }; - var amountBytes = bytes.Skip(trxTypeLength + accountLength + memoLength).Take(amountLength).ToArray(); - var amountDeserial = CcdAmount.TryDeserial(amountBytes, out var amount); - - if (!amountDeserial) + var amountBytes = bytes[(trxTypeLength + accountLength + memoLength)..]; + if (!CcdAmount.TryDeserial(amountBytes, out var amount)) { output = (null, amount.Error); return false; }; - output = (new TransferWithMemo(amount.accountAddress.Value, account.accountAddress, memo.accountAddress), null); + if (amount.Amount == null || account.AccountAddress == null || memo.OnChainData == null) + { + var msg = $"The parsed output is null, but no error was found. This should not be possible."; + output = (null, msg); + return false; + }; + + output = (new TransferWithMemo(amount.Amount.Value, account.AccountAddress, memo.OnChainData), null); return true; } diff --git a/src/Types/AccountAddress.cs b/src/Types/AccountAddress.cs index a07f199f..ee6751c9 100644 --- a/src/Types/AccountAddress.cs +++ b/src/Types/AccountAddress.cs @@ -224,16 +224,16 @@ public Grpc.V2.AccountIdentifierInput ToAccountIdentifierInput() => /// /// The serialized account address. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (AccountAddress? accountAddress, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (AccountAddress? AccountAddress, string? Error) output) { - if (bytes.Length != 32) + if (bytes.Length < BytesLength) { - var msg = $"Invalid length of input in `AccountAddress.TryDeserial`. Expected 32, found {bytes.Length}"; + var msg = $"Invalid length of input in `AccountAddress.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; output = (null, msg); return false; }; - output = (new AccountAddress(bytes.ToArray()), null); + output = (new AccountAddress(bytes[..(int)BytesLength].ToArray()), null); return true; } diff --git a/src/Types/CcdAmount.cs b/src/Types/CcdAmount.cs index 458befa5..a632e18f 100644 --- a/src/Types/CcdAmount.cs +++ b/src/Types/CcdAmount.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using Concordium.Sdk.Helpers; namespace Concordium.Sdk.Types; @@ -115,25 +116,18 @@ public static CcdAmount FromCcd(ulong ccd) /// /// The serialized CCD amount. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (CcdAmount? accountAddress, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (CcdAmount? Amount, string? Error) output) { - if (bytes.Length != BytesLength) + if (bytes.Length < BytesLength) { - var msg = $"Invalid length of input in `CcdAmount.TryDeserial`. Expected {BytesLength}, found {bytes.Length}"; + var msg = $"Invalid length of input in `CcdAmount.TryDeserial`. Expected at least {BytesLength}, found {bytes.Length}"; output = (null, msg); return false; }; - // This call also verifies the length - var u64Deserial = Deserial.TryDeserialU64(bytes, 0, out var amount); + var amount = BinaryPrimitives.ReadUInt64BigEndian(bytes); - if (!u64Deserial) - { - output = (null, amount.Error); - return false; - }; - - output = (new CcdAmount(amount.Ulong.Value), null); + output = (new CcdAmount(amount), null); return true; } diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index eb2f6fbd..2e9b1e13 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using System.Formats.Cbor; using Concordium.Sdk.Helpers; @@ -140,11 +141,11 @@ public byte[] ToBytes() /// /// The serialized "OnChainData". /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (OnChainData? OnChainData, string? Error) output) { - if (bytes.Length == 0) + if (bytes.Length < sizeof(ushort)) { - var msg = "Invalid length of input in `OnChainData.TryDeserial`. Length must be more than 0"; + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must be more than {sizeof(ushort)}"; output = (null, msg); return false; }; @@ -156,23 +157,16 @@ public static bool TryDeserial(byte[] bytes, out (OnChainData? accountAddress, s return false; }; - var deserialSuccess = Deserial.TryDeserialU16(bytes, 0, out var sizeRead); - if (!deserialSuccess) + var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); + var size = sizeRead + sizeof(ushort); + if (size > bytes.Length) { - output = (null, sizeRead.Error); - return false; - }; - - var size = sizeRead.Uint + sizeof(ushort); - - if (bytes.Length != size) - { - var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Expected array of size {size}, found {bytes.Length}"; + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Expected array of size at least {size}, found {bytes.Length}"; output = (null, msg); return false; }; - output = (new OnChainData(bytes.Skip(sizeof(ushort)).ToArray()), null); + output = (new OnChainData(bytes.Slice(sizeof(ushort), sizeRead).ToArray()), null); return true; } diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 1c9de172..1de5653a 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -1,3 +1,4 @@ +using System.Buffers.Binary; using Concordium.Sdk.Exceptions; using Concordium.Sdk.Helpers; @@ -54,36 +55,32 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version /// /// The serialized schema. /// Where to write the result of the operation. - public static bool TryDeserial(byte[] bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) + public static bool TryDeserial(ReadOnlySpan bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) { - var versionSuccess = Deserial.TryDeserialU32(bytes, 0, out var version); - - if (!versionSuccess) + if (bytes.Length < 2 * sizeof(int)) { - output = (null, version.Error); - return false; - } - if (bytes.Length < 8) - { - output = (null, "The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short"); + output = (null, $"The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short. Must be longer than {2 * sizeof(int)}."); return false; } - var rest = bytes.Skip(8).ToArray(); + var version = BinaryPrimitives.ReadUInt32BigEndian(bytes); + var length = BinaryPrimitives.ReadUInt32BigEndian(bytes[sizeof(int)..]); + + var rest = bytes.Slice(2 * sizeof(int), (int)length).ToArray(); - if (version.Uint == 0) + if (version == 0) { output = (ModuleV0.From(rest), null); return true; } - else if (version.Uint == 1) + else if (version == 1) { output = (ModuleV1.From(rest), null); return true; } else { - output = (null, $"Invalid module version byte, expected 0 or 1 but found {version.Uint}"); + output = (null, $"Invalid module version byte, expected 0 or 1 but found {version}"); return false; }; } diff --git a/tests/UnitTests/Transactions/DeployModule.cs b/tests/UnitTests/Transactions/DeployModule.cs index 19220b32..0d2e96e2 100644 --- a/tests/UnitTests/Transactions/DeployModule.cs +++ b/tests/UnitTests/Transactions/DeployModule.cs @@ -46,9 +46,15 @@ public void ToBytes_InverseOfFromBytes() { var moduleBytes = CreateDeployModule().ToBytes(); - var deserialSuccess = DeployModule.TryDeserial(moduleBytes, out var module); + if (DeployModule.TryDeserial(moduleBytes, out var module)) + { + CreateDeployModule().Should().Be(module.Module); + } + else + { + Assert.Fail(module.Error); + } - CreateDeployModule().Should().Be(module.Module); } [Fact] diff --git a/tests/UnitTests/Transactions/RegisterDataTests.cs b/tests/UnitTests/Transactions/RegisterDataTests.cs index 89b64785..00d29ea3 100644 --- a/tests/UnitTests/Transactions/RegisterDataTests.cs +++ b/tests/UnitTests/Transactions/RegisterDataTests.cs @@ -31,8 +31,14 @@ public void ToBytes_ReturnsCorrectValue() public void ToBytes_TryDeserialIsInverse() { var registerData = CreateRegisterData(); - var result = RegisterData.TryDeserial(registerData.ToBytes(), out var registerDataDeserial); - registerData.Should().Be(registerDataDeserial.Item1); + if (RegisterData.TryDeserial(registerData.ToBytes(), out var deserial)) + { + registerData.Should().Be(deserial.RegisterData); + } + else + { + Assert.Fail(deserial.Error); + } } [Fact] diff --git a/tests/UnitTests/Transactions/TransferTests.cs b/tests/UnitTests/Transactions/TransferTests.cs index 3d7c20bb..84b0bd42 100644 --- a/tests/UnitTests/Transactions/TransferTests.cs +++ b/tests/UnitTests/Transactions/TransferTests.cs @@ -75,11 +75,14 @@ public void ToBytes_ReturnsCorrectValue() [Fact] public void ToBytes_InverseOfFromBytes() { - var transferBytes = CreateTransfer().ToBytes(); - - var deserialSuccess = Transfer.TryDeserial(transferBytes, out var transfer); - - CreateTransfer().Should().Be(transfer.Item1); + if (Transfer.TryDeserial(CreateTransfer().ToBytes(), out var deserial)) + { + CreateTransfer().Should().Be(deserial.Transfer); + } + else + { + Assert.Fail(deserial.Error); + } } [Fact] diff --git a/tests/UnitTests/Transactions/TransferWithMemoTests.cs b/tests/UnitTests/Transactions/TransferWithMemoTests.cs index 18ebeb23..87b55eff 100644 --- a/tests/UnitTests/Transactions/TransferWithMemoTests.cs +++ b/tests/UnitTests/Transactions/TransferWithMemoTests.cs @@ -88,8 +88,14 @@ public void ToBytes_ReturnsCorrectValue() public void ToBytes_TryDeserialIsInverse() { var transfer = CreateTransferWithMemo(); - var transferResult = TransferWithMemo.TryDeserial(transfer.ToBytes(), out var transferDeserial); - transfer.Should().Be(transferDeserial.Item1); + if (TransferWithMemo.TryDeserial(transfer.ToBytes(), out var deserial)) + { + transfer.Should().Be(deserial.Transfer); + } + else + { + Assert.Fail(deserial.Error); + }; } [Fact] From 6864086bf7fb0084a9ef33c1d2397c752cd49144 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 5 Dec 2023 15:17:58 +0100 Subject: [PATCH 15/21] handle "impossible" serialization cases with exception --- src/Exceptions/DeserialException.cs | 13 ++++++++++++- src/Transactions/AccountTransactionPayload.cs | 7 ++++++- src/Transactions/DeployModule.cs | 5 ++--- src/Transactions/RegisterData.cs | 5 ++--- src/Transactions/Transfer.cs | 5 ++--- src/Transactions/TransferWithMemo.cs | 5 ++--- 6 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Exceptions/DeserialException.cs b/src/Exceptions/DeserialException.cs index c5cef779..7119f69c 100644 --- a/src/Exceptions/DeserialException.cs +++ b/src/Exceptions/DeserialException.cs @@ -3,10 +3,21 @@ namespace Concordium.Sdk.Exceptions; /// /// Thrown when deserialization fails and is explicitly meant not to. /// -public sealed class DeserialException : Exception +public class DeserialException : Exception { internal DeserialException(string errorMessage) : base($"Deserialization error: {errorMessage}") { } } +/// +/// Thrown when deserialization fails but no error message is present. This +/// should, by construction, be impossible. +/// +public sealed class DeserialInvalidResultException : DeserialException +{ + internal DeserialInvalidResultException() : + base($"Deserialization error: The parsed output is null, but no error was found. This should not be possible.") + { } +} + diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 366b21cf..c57ed519 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -95,15 +95,20 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr case TransactionType.TransferWithScheduleAndMemo: case TransactionType.ConfigureBaker: case TransactionType.ConfigureDelegation: - default: parsedPayload = (new RawPayload(payload.ToArray()), null); break; + default: + throw new MissingEnumException((TransactionType)payload.First()); }; if (parsedPayload.Item2 != null) { throw new DeserialException(parsedPayload.Item2); } + if (parsedPayload.Item1 == null) + { + throw new DeserialInvalidResultException(); + } return parsedPayload.Item1; } } diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 3a55951c..b0a430e5 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -1,3 +1,4 @@ +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -77,9 +78,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (DeployModule? Modu if (module.VersionedModuleSource == null) { - var msg = $"The parsed output is null, but no error was found. This should not be possible."; - output = (null, msg); - return false; + throw new DeserialInvalidResultException(); } output = (new DeployModule(module.VersionedModuleSource), null); diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index 2c31a061..b33405db 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -1,3 +1,4 @@ +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -80,9 +81,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (RegisterData? Regi if (memo.OnChainData == null) { - var msg = $"The parsed output is null, but no error was found. This should not be possible."; - output = (null, msg); - return false; + throw new DeserialInvalidResultException(); }; output = (new RegisterData(memo.OnChainData), null); diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index f478e253..da7afb1a 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -1,3 +1,4 @@ +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -92,9 +93,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Transfer? Transfer if (amount.Amount == null || account.AccountAddress == null) { - var msg = $"The parsed output is null, but no error was found. This should not be possible."; - output = (null, msg); - return false; + throw new DeserialInvalidResultException(); } output = (new Transfer(amount.Amount.Value, account.AccountAddress), null); diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index e0164aa7..ec482c14 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -108,9 +109,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (TransferWithMemo? if (amount.Amount == null || account.AccountAddress == null || memo.OnChainData == null) { - var msg = $"The parsed output is null, but no error was found. This should not be possible."; - output = (null, msg); - return false; + throw new DeserialInvalidResultException(); }; output = (new TransferWithMemo(amount.Amount.Value, account.AccountAddress, memo.OnChainData), null); From ed878ad2e77856199c184f5eb4da8cff74e7a1fb Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 5 Dec 2023 15:47:42 +0100 Subject: [PATCH 16/21] updated changelog and example --- CHANGELOG.md | 2 ++ examples/GetBlockItems/Program.cs | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5028a231..b8390ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ - Added - New GRPC-endpoints: `GetBlocks`, `GetFinalizedBlocks`, `GetBranches`, `GetAncestors`, `GetBlockPendingUpdates`, `GetBlockItems` - New transaction `DeployModule` + - The function `Prepare` has been removed from the `AccountTransactionPayload` class, but is implemented for all subclasses except `RawPayload`. + - The function `GetTransactionSpecificCost` has been removed from the `AccountTransactionPayload` class. - Added serialization and deserialization for all instances of `AccountTransactionPayload` - Added helpers to get new type `ContractIdentifier` on `ReceiveName` and `ContractName`. This new type only holds the contract name part of `ReceiveName` and `ContractName`. Also added helper to get entrypoint on `ReceiveName`. diff --git a/examples/GetBlockItems/Program.cs b/examples/GetBlockItems/Program.cs index 898ee79c..faf45245 100644 --- a/examples/GetBlockItems/Program.cs +++ b/examples/GetBlockItems/Program.cs @@ -37,12 +37,14 @@ private static async Task Run(GetBlocksOptions o) IBlockHashInput bi = o.BlockHash != null ? new Given(BlockHash.From(o.BlockHash)) : new LastFinal(); - var blockItems = client.GetBlockItems(bi); - - await foreach (var item in blockItems) + var blockItems = await client.GetBlockItems(bi); + + Console.WriteLine($"All block items in block {blockItems.BlockHash}: ["); + await foreach (var item in blockItems.Response) { - Console.WriteLine($"Blockitem: {item}"); + Console.WriteLine($"{item},"); } + Console.WriteLine("]"); } } From c3f3e6f93f0b947a23b692c5031989857ea6b981 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 12 Dec 2023 11:36:05 +0100 Subject: [PATCH 17/21] Addressed comment: Fixed PayloadSize calculation --- src/Transactions/AccountTransactionHeader.cs | 4 ++-- src/Transactions/AccountTransactionPayload.cs | 5 ++++ src/Transactions/DeployModule.cs | 9 ++++++-- src/Transactions/RawPayload.cs | 7 ++++++ src/Transactions/RegisterData.cs | 5 ++++ src/Transactions/SignedAccountTransaction.cs | 23 +++++++++++-------- src/Transactions/Transfer.cs | 5 ++++ src/Transactions/TransferWithMemo.cs | 9 ++++++++ src/Types/OnChainData.cs | 5 ++++ src/Types/VersionedModuleSource.cs | 7 ++++-- 10 files changed, 63 insertions(+), 16 deletions(-) diff --git a/src/Transactions/AccountTransactionHeader.cs b/src/Transactions/AccountTransactionHeader.cs index 43777492..3bdae295 100644 --- a/src/Transactions/AccountTransactionHeader.cs +++ b/src/Transactions/AccountTransactionHeader.cs @@ -91,11 +91,11 @@ public Grpc.V2.AccountTransactionHeader ToProto() => /// /// Creates an account transaction header from its corresponding protocol buffer message instance. /// - internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader) => new( + internal static AccountTransactionHeader From(Grpc.V2.AccountTransactionHeader accountTransactionHeader, PayloadSize payloadSize) => new( AccountAddress.From(accountTransactionHeader.Sender), AccountSequenceNumber.From(accountTransactionHeader.SequenceNumber), Expiry.From(accountTransactionHeader.Expiry.Value), EnergyAmount.From(accountTransactionHeader.EnergyAmount), - new PayloadSize((uint)accountTransactionHeader.CalculateSize()) + payloadSize ); } diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index c57ed519..34367372 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -18,6 +18,11 @@ public abstract record AccountTransactionPayload /// public abstract byte[] ToBytes(); + /// + /// Gets the size (number of bytes) of the payload. + /// + internal abstract PayloadSize Size(); + /// /// Converts the transaction to its corresponding protocol buffer message instance. /// diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index b0a430e5..a710f39f 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -27,11 +27,16 @@ Expiry expiry /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(Module.GetBytesLength() / 10); + private readonly EnergyAmount _transactionCost = new(Module.Length() / 10); /// The account transaction type to be used in the serialized payload. private const byte TransactionType = (byte)Types.TransactionType.DeployModule; + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new(this.Module.Length() + sizeof(TransactionType)); + /// /// Copies the "deploy module" account transaction in the binary format expected by the node to a byte array. /// @@ -40,7 +45,7 @@ private static byte[] Serialize(VersionedModuleSource module) { using var memoryStream = new MemoryStream((int)( sizeof(TransactionType) + - module.GetBytesLength() + module.Length() )); memoryStream.WriteByte(TransactionType); memoryStream.Write(module.ToBytes()); diff --git a/src/Transactions/RawPayload.cs b/src/Transactions/RawPayload.cs index 8277209f..6e6f8573 100644 --- a/src/Transactions/RawPayload.cs +++ b/src/Transactions/RawPayload.cs @@ -1,3 +1,5 @@ +using Concordium.Sdk.Types; + namespace Concordium.Sdk.Transactions; /// @@ -10,5 +12,10 @@ namespace Concordium.Sdk.Transactions; /// The raw bytes of the payload. public sealed record RawPayload(byte[] Bytes) : AccountTransactionPayload { + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new((uint)this.Bytes.Length); + public override byte[] ToBytes() => this.Bytes; } diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index b33405db..057f4641 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -37,6 +37,11 @@ Expiry expiry /// private readonly EnergyAmount _transactionCost = new(300); + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new(sizeof(TransactionType) + this.Data.Length()); + /// /// Copies the "register data" account transaction in the binary format expected by the node to a byte array. /// diff --git a/src/Transactions/SignedAccountTransaction.cs b/src/Transactions/SignedAccountTransaction.cs index ea7397f4..fc4b65d3 100644 --- a/src/Transactions/SignedAccountTransaction.cs +++ b/src/Transactions/SignedAccountTransaction.cs @@ -22,19 +22,22 @@ AccountTransactionSignature Signature ) : BlockItemType { /// Converts this type to the equivalent protocol buffer type. - public AccountTransaction ToProto() => - new() - { - Header = this.Header.ToProto(), - Payload = this.Payload.ToProto(), - Signature = this.Signature.ToProto(), - }; + public AccountTransaction ToProto() => new() + { + Header = this.Header.ToProto(), + Payload = this.Payload.ToProto(), + Signature = this.Signature.ToProto(), + }; - internal static SignedAccountTransaction From(AccountTransaction accountTransaction) => new( - AccountTransactionHeader.From(accountTransaction.Header), - AccountTransactionPayload.From(accountTransaction.Payload), + internal static SignedAccountTransaction From(AccountTransaction accountTransaction) + { + var payload = AccountTransactionPayload.From(accountTransaction.Payload); + return new( + AccountTransactionHeader.From(accountTransaction.Header, payload.Size()), + payload, AccountTransactionSignature.From(accountTransaction.Signature) ); + } /// /// Converts the signed account transaction to a protocol buffer diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index da7afb1a..8d7564be 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -43,6 +43,11 @@ Expiry expiry /// private readonly EnergyAmount _transactionCost = new(300); + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new(BytesLength); + /// /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. /// diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index ec482c14..cb27d5ac 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -41,6 +41,15 @@ Expiry expiry /// private readonly EnergyAmount _transactionCost = new(300); + /// + /// Gets the size (number of bytes) of the payload. + /// + internal override PayloadSize Size() => new( + this.Memo.Length() + + sizeof(TransactionType) + + CcdAmount.BytesLength + + AccountAddress.BytesLength); + /// /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. /// diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index 2e9b1e13..cbf2bfa0 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -29,6 +29,11 @@ public sealed record OnChainData : IEquatable /// Data represented by at most bytes. private OnChainData(byte[] bytes) => this._value = bytes; + /// + /// Gets the length (number of bytes) of the data. + /// + internal uint Length() => (uint)this._value.Length; + /// /// Creates an instance from a hex encoded string. /// diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index 1de5653a..fd1cfe6e 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -13,11 +13,14 @@ public abstract record VersionedModuleSource(byte[] Source) : IEquatable (uint)((2 * sizeof(int)) + this.Source.Length); + /// + /// Gets the length (number of bytes) of the Module. + /// + internal uint Length() => (uint)((2 * sizeof(int)) + this.Source.Length); internal byte[] ToBytes() { - using var memoryStream = new MemoryStream((int)this.GetBytesLength()); + using var memoryStream = new MemoryStream((int)this.Length()); memoryStream.Write(Serialization.ToBytes(this.GetVersion())); memoryStream.Write(Serialization.ToBytes((uint)this.Source.Length)); memoryStream.Write(this.Source); From eefd1fd9b4c607c083851004f987dc9fffa540d6 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 19 Dec 2023 15:58:21 +0100 Subject: [PATCH 18/21] Addressed comments --- src/Exceptions/DeserialException.cs | 4 +- src/Transactions/AccountTransactionPayload.cs | 4 +- src/Transactions/CredentialDeployment.cs | 2 +- src/Transactions/DeployModule.cs | 36 +++++++---------- src/Transactions/RegisterData.cs | 32 +++++++-------- src/Transactions/Transfer.cs | 30 +++++++------- src/Transactions/TransferWithMemo.cs | 39 +++++++------------ src/Transactions/UpdateInstruction.cs | 2 +- src/Types/OnChainData.cs | 22 +++++------ src/Types/VersionedModuleSource.cs | 15 ++++--- 10 files changed, 83 insertions(+), 103 deletions(-) diff --git a/src/Exceptions/DeserialException.cs b/src/Exceptions/DeserialException.cs index 7119f69c..067ec8e4 100644 --- a/src/Exceptions/DeserialException.cs +++ b/src/Exceptions/DeserialException.cs @@ -14,9 +14,9 @@ internal DeserialException(string errorMessage) : /// Thrown when deserialization fails but no error message is present. This /// should, by construction, be impossible. /// -public sealed class DeserialInvalidResultException : DeserialException +public sealed class DeserialNullException : DeserialException { - internal DeserialInvalidResultException() : + internal DeserialNullException() : base($"Deserialization error: The parsed output is null, but no error was found. This should not be possible.") { } } diff --git a/src/Transactions/AccountTransactionPayload.cs b/src/Transactions/AccountTransactionPayload.cs index 34367372..67617473 100644 --- a/src/Transactions/AccountTransactionPayload.cs +++ b/src/Transactions/AccountTransactionPayload.cs @@ -49,7 +49,7 @@ public Grpc.V2.AccountTransactionPayload ToProto() => PayloadCase.RawPayload => ParseRawPayload(payload.RawPayload), PayloadCase.InitContract => throw new NotImplementedException(), PayloadCase.UpdateContract => throw new NotImplementedException(), - PayloadCase.None => throw new NotImplementedException(), + PayloadCase.None => throw new MissingEnumException(payload.PayloadCase), _ => throw new MissingEnumException(payload.PayloadCase), }; @@ -112,7 +112,7 @@ private static AccountTransactionPayload ParseRawPayload(Google.Protobuf.ByteStr } if (parsedPayload.Item1 == null) { - throw new DeserialInvalidResultException(); + throw new DeserialNullException(); } return parsedPayload.Item1; } diff --git a/src/Transactions/CredentialDeployment.cs b/src/Transactions/CredentialDeployment.cs index 7b20a316..b53d905b 100644 --- a/src/Transactions/CredentialDeployment.cs +++ b/src/Transactions/CredentialDeployment.cs @@ -19,7 +19,7 @@ internal static CredentialDeployment From(Grpc.V2.CredentialDeployment cred) => cred.PayloadCase switch { CredentialDeploymentPayloadCase.RawPayload => new CredentialPayloadRaw(cred.RawPayload.ToByteArray()), - CredentialDeploymentPayloadCase.None => throw new NotImplementedException(), + CredentialDeploymentPayloadCase.None => throw new MissingEnumException(cred.PayloadCase), _ => throw new MissingEnumException(cred.PayloadCase), } ); diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index a710f39f..644076b4 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -27,7 +27,7 @@ Expiry expiry /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(Module.Length() / 10); + private readonly EnergyAmount _transactionCost = new(Module.SerializedLength() / 10); /// The account transaction type to be used in the serialized payload. private const byte TransactionType = (byte)Types.TransactionType.DeployModule; @@ -35,22 +35,7 @@ Expiry expiry /// /// Gets the size (number of bytes) of the payload. /// - internal override PayloadSize Size() => new(this.Module.Length() + sizeof(TransactionType)); - - /// - /// Copies the "deploy module" account transaction in the binary format expected by the node to a byte array. - /// - /// The smart contract module to be deployed. - private static byte[] Serialize(VersionedModuleSource module) - { - using var memoryStream = new MemoryStream((int)( - sizeof(TransactionType) + - module.Length() - )); - memoryStream.WriteByte(TransactionType); - memoryStream.Write(module.ToBytes()); - return memoryStream.ToArray(); - } + internal override PayloadSize Size() => new(this.Module.SerializedLength() + sizeof(TransactionType)); /// /// Create a "deploy module" payload from a serialized as bytes. @@ -62,14 +47,14 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (DeployModule? Modu var minLength = sizeof(TransactionType) + (2 * sizeof(int)); if (bytes.Length < minLength) { - var msg = $"Invalid input length in `DeployModule.TryDeserial`. expected at least {minLength}, found {bytes.Length}"; + var msg = $"Invalid input length in `DeployModule.TryDeserial`. Expected at least {minLength}, found {bytes.Length}"; output = (null, msg); return false; } if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + var msg = $"Invalid transaction type in `DeployModule.TryDeserial`. Expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; } @@ -83,13 +68,22 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (DeployModule? Modu if (module.VersionedModuleSource == null) { - throw new DeserialInvalidResultException(); + throw new DeserialNullException(); } output = (new DeployModule(module.VersionedModuleSource), null); return true; } - public override byte[] ToBytes() => Serialize(this.Module); + /// + /// Copies the "deploy module" account transaction in the binary format expected by the node to a byte array. + /// + public override byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.Module.SerializedLength()); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(this.Module.ToBytes()); + return memoryStream.ToArray(); + } } diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index 057f4641..d5313d81 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -40,21 +40,7 @@ Expiry expiry /// /// Gets the size (number of bytes) of the payload. /// - internal override PayloadSize Size() => new(sizeof(TransactionType) + this.Data.Length()); - - /// - /// Copies the "register data" account transaction in the binary format expected by the node to a byte array. - /// - /// The data to be registered on-chain. - private static byte[] Serialize(OnChainData data) - { - var buffer = data.ToBytes(); - var size = sizeof(TransactionType) + buffer.Length; - using var memoryStream = new MemoryStream(size); - memoryStream.WriteByte(TransactionType); - memoryStream.Write(buffer); - return memoryStream.ToArray(); - } + internal override PayloadSize Size() => new(sizeof(TransactionType) + this.Data.SerializedLength()); /// /// Create a "register data" payload from a serialized as bytes. @@ -72,7 +58,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (RegisterData? Regi }; if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. Expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; }; @@ -86,12 +72,22 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (RegisterData? Regi if (memo.OnChainData == null) { - throw new DeserialInvalidResultException(); + throw new DeserialNullException(); }; output = (new RegisterData(memo.OnChainData), null); return true; } - public override byte[] ToBytes() => Serialize(this.Data); + /// + /// Copies the "register data" account transaction in the binary format expected by the node to a byte array. + /// + public override byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.Size().Size); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(this.Data.ToBytes()); + return memoryStream.ToArray(); + } + } diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index 8d7564be..184849de 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -48,20 +48,6 @@ Expiry expiry /// internal override PayloadSize Size() => new(BytesLength); - /// - /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. - /// - /// Amount to send. - /// Address of the receiver account to which the amount will be sent. - private static byte[] Serialize(CcdAmount amount, AccountAddress receiver) - { - using var memoryStream = new MemoryStream((int)BytesLength); - memoryStream.WriteByte(TransactionType); - memoryStream.Write(receiver.ToBytes()); - memoryStream.Write(amount.ToBytes()); - return memoryStream.ToArray(); - } - /// /// Create a "transfer" payload from a serialized as bytes. /// @@ -77,7 +63,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Transfer? Transfer }; if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. Expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; }; @@ -98,12 +84,22 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Transfer? Transfer if (amount.Amount == null || account.AccountAddress == null) { - throw new DeserialInvalidResultException(); + throw new DeserialNullException(); } output = (new Transfer(amount.Amount.Value, account.AccountAddress), null); return true; } - public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver); + /// + /// Copies the "transfer" account transaction in the binary format expected by the node to a byte array. + /// + public override byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)BytesLength); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(this.Receiver.ToBytes()); + memoryStream.Write(this.Amount.ToBytes()); + return memoryStream.ToArray(); + } } diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index cb27d5ac..21eb2954 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -45,31 +45,11 @@ Expiry expiry /// Gets the size (number of bytes) of the payload. /// internal override PayloadSize Size() => new( - this.Memo.Length() + + this.Memo.SerializedLength() + sizeof(TransactionType) + CcdAmount.BytesLength + AccountAddress.BytesLength); - /// - /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. - /// - /// Amount to send. - /// Address of the receiver account to which the amount will be sent. - /// Memo to include with the transaction. - private static byte[] Serialize(CcdAmount amount, AccountAddress receiver, OnChainData memo) - { - using var memoryStream = new MemoryStream((int)( - sizeof(TransactionType) + - CcdAmount.BytesLength + - AccountAddress.BytesLength + - OnChainData.MaxLength)); - memoryStream.WriteByte(TransactionType); - memoryStream.Write(receiver.ToBytes()); - memoryStream.Write(memo.ToBytes()); - memoryStream.Write(amount.ToBytes()); - return memoryStream.ToArray(); - } - /// /// Create a "transfer with memo" payload from a serialized as bytes. /// @@ -86,7 +66,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (TransferWithMemo? }; if (bytes[0] != TransactionType) { - var msg = $"Invalid transaction type in `Transfer.TryDeserial`. expected {TransactionType}, found {bytes[0]}"; + var msg = $"Invalid transaction type in `Transfer.TryDeserial`. Expected {TransactionType}, found {bytes[0]}"; output = (null, msg); return false; }; @@ -118,12 +98,23 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (TransferWithMemo? if (amount.Amount == null || account.AccountAddress == null || memo.OnChainData == null) { - throw new DeserialInvalidResultException(); + throw new DeserialNullException(); }; output = (new TransferWithMemo(amount.Amount.Value, account.AccountAddress, memo.OnChainData), null); return true; } - public override byte[] ToBytes() => Serialize(this.Amount, this.Receiver, this.Memo); + /// + /// Copies the "transfer with memo" account transaction in the binary format expected by the node to a byte array. + /// + public override byte[] ToBytes() + { + using var memoryStream = new MemoryStream((int)this.Size().Size); + memoryStream.WriteByte(TransactionType); + memoryStream.Write(this.Receiver.ToBytes()); + memoryStream.Write(this.Memo.ToBytes()); + memoryStream.Write(this.Amount.ToBytes()); + return memoryStream.ToArray(); + } } diff --git a/src/Transactions/UpdateInstruction.cs b/src/Transactions/UpdateInstruction.cs index 6e61c24b..9dc2579f 100644 --- a/src/Transactions/UpdateInstruction.cs +++ b/src/Transactions/UpdateInstruction.cs @@ -24,7 +24,7 @@ internal static UpdateInstruction From(Grpc.V2.UpdateInstruction updateInstructi updateInstruction.Payload.PayloadCase switch { UpdateInstructionPayloadCase.RawPayload => new UpdateInstructionPayloadRaw(updateInstruction.Payload.RawPayload.ToByteArray()), - UpdateInstructionPayloadCase.None => throw new NotImplementedException(), + UpdateInstructionPayloadCase.None => throw new MissingEnumException(updateInstruction.Payload.PayloadCase), _ => throw new MissingEnumException(updateInstruction.Payload.PayloadCase), } ); diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index cbf2bfa0..d2d14b80 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -18,6 +18,11 @@ public sealed record OnChainData : IEquatable /// public const uint MaxLength = 256; + /// + /// The minimum serialized length. + /// + internal const uint MinSerializedLength = sizeof(ushort); + /// /// Byte array representing the data. /// @@ -30,9 +35,9 @@ public sealed record OnChainData : IEquatable private OnChainData(byte[] bytes) => this._value = bytes; /// - /// Gets the length (number of bytes) of the data. + /// Gets the length (number of bytes) of the serializd data. /// - internal uint Length() => (uint)this._value.Length; + internal uint SerializedLength() => (uint)this._value.Length + sizeof(ushort); /// /// Creates an instance from a hex encoded string. @@ -130,7 +135,7 @@ public static OnChainData From(byte[] dataAsBytes) /// public byte[] ToBytes() { - using var memoryStream = new MemoryStream(sizeof(ushort) + this._value.Length); + using var memoryStream = new MemoryStream((int)this.SerializedLength()); memoryStream.Write(Serialization.ToBytes((ushort)this._value.Length)); memoryStream.Write(this._value); return memoryStream.ToArray(); @@ -148,16 +153,9 @@ public byte[] ToBytes() /// Where to write the result of the operation. public static bool TryDeserial(ReadOnlySpan bytes, out (OnChainData? OnChainData, string? Error) output) { - if (bytes.Length < sizeof(ushort)) - { - var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must be more than {sizeof(ushort)}"; - output = (null, msg); - return false; - }; - - if (bytes.Length > sizeof(ushort) + MaxLength) + if (bytes.Length < MinSerializedLength) { - var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must not be more than {sizeof(ushort) + MaxLength}"; + var msg = $"Invalid length of input in `OnChainData.TryDeserial`. Length must be more than {MinSerializedLength}"; output = (null, msg); return false; }; diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index e94e132c..cfd0dd39 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -5,6 +5,9 @@ namespace Concordium.Sdk.Types; +/// +/// Contains source code of a versioned module where inherited classes are concrete versions. +/// public abstract record VersionedModuleSource { /// @@ -18,9 +21,11 @@ public abstract record VersionedModuleSource internal abstract uint GetVersion(); /// - /// Gets the length (number of bytes) of the Module. + /// Gets the length (number of bytes) of the serialized Module. /// - internal uint Length() => (uint)((2 * sizeof(int)) + this.Source.Length); + internal uint SerializedLength() => (uint)((2 * sizeof(int)) + this.Source.Length); + + internal const uint MinSerializedLength = 2 * sizeof(int); /// /// Base constructor @@ -51,7 +56,7 @@ private Module GetWasmModule() internal byte[] ToBytes() { - using var memoryStream = new MemoryStream((int)this.Length()); + using var memoryStream = new MemoryStream((int)this.SerializedLength()); memoryStream.Write(Serialization.ToBytes(this.GetVersion())); memoryStream.Write(Serialization.ToBytes((uint)this.Source.Length)); memoryStream.Write(this.Source); @@ -129,9 +134,9 @@ internal static VersionedModuleSource From(Grpc.V2.VersionedModuleSource version /// Where to write the result of the operation. public static bool TryDeserial(ReadOnlySpan bytes, out (VersionedModuleSource? VersionedModuleSource, string? Error) output) { - if (bytes.Length < 2 * sizeof(int)) + if (bytes.Length < VersionedModuleSource.MinSerializedLength) { - output = (null, $"The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short. Must be longer than {2 * sizeof(int)}."); + output = (null, $"The given byte array in `VersionModuleSourceFactory.TryDeserial`, is too short. Must be longer than {VersionedModuleSource.MinSerializedLength}."); return false; } From 466ce4982bf75861ebbeb388d6423dbe66faa954 Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 14:29:54 +0100 Subject: [PATCH 19/21] Addressed comments --- .../UnexpectedNodeResponseException.cs | 11 +++++ src/Helpers/Deserialization.cs | 1 - src/Transactions/AccountSignatureMap.cs | 44 +++++++++++++++++++ src/Transactions/BlockItem.cs | 2 +- src/Transactions/DeployModule.cs | 5 ++- src/Transactions/RegisterData.cs | 9 ++-- src/Transactions/Transfer.cs | 9 ++-- src/Transactions/TransferWithMemo.cs | 9 ++-- src/Types/OnChainData.cs | 3 ++ src/Types/VersionedModuleSource.cs | 20 ++++----- 10 files changed, 86 insertions(+), 27 deletions(-) create mode 100644 src/Exceptions/UnexpectedNodeResponseException.cs delete mode 100644 src/Helpers/Deserialization.cs diff --git a/src/Exceptions/UnexpectedNodeResponseException.cs b/src/Exceptions/UnexpectedNodeResponseException.cs new file mode 100644 index 00000000..60dc8b71 --- /dev/null +++ b/src/Exceptions/UnexpectedNodeResponseException.cs @@ -0,0 +1,11 @@ +namespace Concordium.Sdk.Exceptions; + +/// +/// Thrown if the node sends an invalid response. +/// +public class UnexpectedNodeResponseException : Exception +{ + internal UnexpectedNodeResponseException() : + base($"Unexpected node response received.") + { } +} diff --git a/src/Helpers/Deserialization.cs b/src/Helpers/Deserialization.cs deleted file mode 100644 index 8b137891..00000000 --- a/src/Helpers/Deserialization.cs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index 68414525..c00c3e6a 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -66,6 +66,28 @@ internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) } return Create(dict); } + + /// Check for equality. + public bool Equals(AccountSignatureMap? other) => other != null && + other.GetType().Equals(this.GetType()) && + this.Signatures.Count == other.Signatures.Count && + this.Signatures.All(p => p.Value == other.Signatures[p.Key]); + + /// Gets hash code. + public override int GetHashCode() + { + // Based on https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations + unchecked + { + var hash = 17; + foreach (var (key, val) in this.Signatures) + { + hash = (hash * 31) + key.GetHashCode(); + hash = (hash * 31) + Helpers.HashCode.GetHashCodeByteArray(val); + } + return hash; + } + } } @@ -118,4 +140,26 @@ internal static UpdateInstructionSignatureMap From(Grpc.V2.SignatureMap map) } return Create(dict); } + + /// Check for equality. + public bool Equals(UpdateInstructionSignatureMap? other) => other != null && + other.GetType().Equals(this.GetType()) && + this.Signatures.Count == other.Signatures.Count && + this.Signatures.All(p => p.Value == other.Signatures[p.Key]); + + /// Gets hash code. + public override int GetHashCode() + { + // Based on https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations + unchecked + { + var hash = 17; + foreach (var (key, val) in this.Signatures) + { + hash = (hash * 31) + key.GetHashCode(); + hash = (hash * 31) + Helpers.HashCode.GetHashCodeByteArray(val); + } + return hash; + } + } } diff --git a/src/Transactions/BlockItem.cs b/src/Transactions/BlockItem.cs index 69dfe4c7..a4b74485 100644 --- a/src/Transactions/BlockItem.cs +++ b/src/Transactions/BlockItem.cs @@ -17,7 +17,7 @@ internal static BlockItem From(Grpc.V2.BlockItem blockItem) => BlockItemCase.AccountTransaction => SignedAccountTransaction.From(blockItem.AccountTransaction), BlockItemCase.CredentialDeployment => CredentialDeployment.From(blockItem.CredentialDeployment), BlockItemCase.UpdateInstruction => UpdateInstruction.From(blockItem.UpdateInstruction), - BlockItemCase.None => throw new NotImplementedException(), + BlockItemCase.None => throw new UnexpectedNodeResponseException(), _ => throw new MissingEnumException(blockItem.BlockItemCase), } ); diff --git a/src/Transactions/DeployModule.cs b/src/Transactions/DeployModule.cs index 644076b4..87f69c8d 100644 --- a/src/Transactions/DeployModule.cs +++ b/src/Transactions/DeployModule.cs @@ -1,4 +1,3 @@ -using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -68,7 +67,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (DeployModule? Modu if (module.VersionedModuleSource == null) { - throw new DeserialNullException(); + var msg = $"VersionedModuleSource was null, but did not produce an error"; + output = (null, msg); + return false; } output = (new DeployModule(module.VersionedModuleSource), null); diff --git a/src/Transactions/RegisterData.cs b/src/Transactions/RegisterData.cs index d5313d81..092883eb 100644 --- a/src/Transactions/RegisterData.cs +++ b/src/Transactions/RegisterData.cs @@ -1,4 +1,3 @@ -using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -26,7 +25,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); + ) => new(sender, sequenceNumber, expiry, new EnergyAmount(TrxCost), this); /// /// The transaction specific cost for submitting this type of @@ -35,7 +34,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(300); + private const ushort TrxCost = 300; /// /// Gets the size (number of bytes) of the payload. @@ -72,7 +71,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (RegisterData? Regi if (memo.OnChainData == null) { - throw new DeserialNullException(); + var msg = $"OnChainData was null, but did not produce an error"; + output = (null, msg); + return false; }; output = (new RegisterData(memo.OnChainData), null); diff --git a/src/Transactions/Transfer.cs b/src/Transactions/Transfer.cs index 184849de..7a8c0275 100644 --- a/src/Transactions/Transfer.cs +++ b/src/Transactions/Transfer.cs @@ -1,4 +1,3 @@ -using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -32,7 +31,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); + ) => new(sender, sequenceNumber, expiry, new EnergyAmount(TrxCost), this); /// /// The transaction specific cost for submitting this type of @@ -41,7 +40,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(300); + private const ushort TrxCost = 300; /// /// Gets the size (number of bytes) of the payload. @@ -84,7 +83,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (Transfer? Transfer if (amount.Amount == null || account.AccountAddress == null) { - throw new DeserialNullException(); + var msg = $"Amount or AccountAddress were null, but did not produce an error"; + output = (null, msg); + return false; } output = (new Transfer(amount.Amount.Value, account.AccountAddress), null); diff --git a/src/Transactions/TransferWithMemo.cs b/src/Transactions/TransferWithMemo.cs index 21eb2954..94420917 100644 --- a/src/Transactions/TransferWithMemo.cs +++ b/src/Transactions/TransferWithMemo.cs @@ -1,5 +1,4 @@ using System.Buffers.Binary; -using Concordium.Sdk.Exceptions; using Concordium.Sdk.Types; namespace Concordium.Sdk.Transactions; @@ -30,7 +29,7 @@ public PreparedAccountTransaction Prepare( AccountAddress sender, AccountSequenceNumber sequenceNumber, Expiry expiry - ) => new(sender, sequenceNumber, expiry, this._transactionCost, this); + ) => new(sender, sequenceNumber, expiry, new EnergyAmount(TrxCost), this); /// /// The transaction specific cost for submitting this type of @@ -39,7 +38,7 @@ Expiry expiry /// This should reflect the transaction-specific costs defined here: /// https://github.com/Concordium/concordium-base/blob/78f557b8b8c94773a25e4f86a1a92bc323ea2e3d/haskell-src/Concordium/Cost.hs /// - private readonly EnergyAmount _transactionCost = new(300); + private const ushort TrxCost = 300; /// /// Gets the size (number of bytes) of the payload. @@ -98,7 +97,9 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (TransferWithMemo? if (amount.Amount == null || account.AccountAddress == null || memo.OnChainData == null) { - throw new DeserialNullException(); + var msg = $"Amount, AccountAddress or OnChainData were null, but did not produce an error"; + output = (null, msg); + return false; }; output = (new TransferWithMemo(amount.Amount.Value, account.AccountAddress, memo.OnChainData), null); diff --git a/src/Types/OnChainData.cs b/src/Types/OnChainData.cs index d2d14b80..9191d73c 100644 --- a/src/Types/OnChainData.cs +++ b/src/Types/OnChainData.cs @@ -160,6 +160,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (OnChainData? OnCha return false; }; + // The function below would throw if it were not for the above check. var sizeRead = BinaryPrimitives.ReadUInt16BigEndian(bytes); var size = sizeRead + sizeof(ushort); if (size > bytes.Length) @@ -173,8 +174,10 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (OnChainData? OnCha return true; } + /// Check for equality. public bool Equals(OnChainData? other) => other is not null && this._value.SequenceEqual(other._value); + /// Gets hash code. public override int GetHashCode() => Helpers.HashCode.GetHashCodeByteArray(this._value); internal static OnChainData? From(Grpc.V2.Memo? memo) diff --git a/src/Types/VersionedModuleSource.cs b/src/Types/VersionedModuleSource.cs index cfd0dd39..e3710478 100644 --- a/src/Types/VersionedModuleSource.cs +++ b/src/Types/VersionedModuleSource.cs @@ -140,6 +140,7 @@ public static bool TryDeserial(ReadOnlySpan bytes, out (VersionedModuleSou return false; } + // The functions below would throw if it were not for the above check. var version = BinaryPrimitives.ReadUInt32BigEndian(bytes); var length = BinaryPrimitives.ReadUInt32BigEndian(bytes[sizeof(int)..]); @@ -176,6 +177,8 @@ internal static ModuleV0 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV0 /// /// Creates a WASM-module from byte array. + /// Note: Does not copy the given byte array, so it assumes that the underlying + /// byte array is not mutated /// /// WASM-module as a byte array. /// The length of the supplied module exceeds "MaxLength". @@ -188,7 +191,7 @@ public static ModuleV0 From(byte[] source) ); } - return new ModuleV0(source.ToArray()); + return new ModuleV0(source); } /// @@ -198,15 +201,8 @@ public static ModuleV0 From(byte[] source) /// The supplied string is not a hex encoded WASM-module representing at most "MaxLength" bytes. public static ModuleV0 FromHex(string hexString) { - try - { - var value = Convert.FromHexString(hexString); - return From(value); - } - catch (Exception e) - { - throw new ArgumentException("The provided string is not hex encoded: ", e); - } + var value = Convert.FromHexString(hexString); + return From(value); } private protected override (byte[]? Schema, ModuleSchemaVersion SchemaVersion)? ExtractSchemaFromWebAssemblyModule(Module module) @@ -236,6 +232,8 @@ internal static ModuleV1 From(Grpc.V2.VersionedModuleSource.Types.ModuleSourceV1 /// /// Creates a WASM-module from byte array. + /// Note: Does not copy the given byte array, so it assumes that the underlying + /// byte array is not mutated /// /// WASM-module as a byte array. /// The length of the supplied module exceeds "MaxLength". @@ -248,7 +246,7 @@ public static ModuleV1 From(byte[] source) ); } - return new ModuleV1(source.ToArray()); + return new ModuleV1(source); } /// From 126206fac99272235f0b912a231651aad626364b Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 15:04:11 +0100 Subject: [PATCH 20/21] Made `Helpers.HashCode.GetHashCodeByteArray` unchecked --- src/Helpers/HashCode.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Helpers/HashCode.cs b/src/Helpers/HashCode.cs index 846f7f63..214df4a3 100644 --- a/src/Helpers/HashCode.cs +++ b/src/Helpers/HashCode.cs @@ -7,13 +7,16 @@ public static class HashCode { public static int GetHashCodeByteArray(byte[] array) { - var hashValue = 31; - - foreach (var value in array) + unchecked { - hashValue = (37 * hashValue) + value.GetHashCode(); - } + var hashValue = 31; - return hashValue; + foreach (var value in array) + { + hashValue = (37 * hashValue) + value.GetHashCode(); + } + + return hashValue; + } } } From 6af2857ce8fc3f96bc923328ba5b4690c438078e Mon Sep 17 00:00:00 2001 From: rasmus-kirk Date: Tue, 23 Jan 2024 16:17:25 +0100 Subject: [PATCH 21/21] Addressed more comments --- src/Transactions/AccountSignatureMap.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Transactions/AccountSignatureMap.cs b/src/Transactions/AccountSignatureMap.cs index c00c3e6a..facca1bb 100644 --- a/src/Transactions/AccountSignatureMap.cs +++ b/src/Transactions/AccountSignatureMap.cs @@ -62,6 +62,7 @@ internal static AccountSignatureMap From(Grpc.V2.AccountSignatureMap map) var dict = new Dictionary(); foreach (var s in map.Signatures) { + // The GRPC api states that keys must not exceed 2^8, so this should be safe. dict.Add(new AccountKeyIndex((byte)s.Key), s.Value.Value.ToByteArray()); } return Create(dict); @@ -76,14 +77,13 @@ public bool Equals(AccountSignatureMap? other) => other != null && /// Gets hash code. public override int GetHashCode() { - // Based on https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations unchecked { var hash = 17; foreach (var (key, val) in this.Signatures) { - hash = (hash * 31) + key.GetHashCode(); - hash = (hash * 31) + Helpers.HashCode.GetHashCodeByteArray(val); + hash += key.GetHashCode(); + hash += Helpers.HashCode.GetHashCodeByteArray(val); } return hash; } @@ -136,6 +136,7 @@ internal static UpdateInstructionSignatureMap From(Grpc.V2.SignatureMap map) var dict = new Dictionary(); foreach (var s in map.Signatures) { + // The GRPC api states that keys must not exceed 2^8, so this should be safe. dict.Add(new UpdateKeysIndex((byte)s.Key), s.Value.Value.ToByteArray()); } return Create(dict); @@ -150,14 +151,13 @@ public bool Equals(UpdateInstructionSignatureMap? other) => other != null && /// Gets hash code. public override int GetHashCode() { - // Based on https://stackoverflow.com/questions/1646807/quick-and-simple-hash-code-combinations unchecked { var hash = 17; foreach (var (key, val) in this.Signatures) { - hash = (hash * 31) + key.GetHashCode(); - hash = (hash * 31) + Helpers.HashCode.GetHashCodeByteArray(val); + hash += key.GetHashCode(); + hash += Helpers.HashCode.GetHashCodeByteArray(val); } return hash; }