From ba771836fb8490d590b68a25781f46cfee924913 Mon Sep 17 00:00:00 2001 From: Ulf Wiger Date: Fri, 11 Apr 2025 16:39:02 +0200 Subject: [PATCH] Document static, make anyint standard --- README.md | 19 +++++++++-- doc/static.md | 83 +++++++++++++++++++++++++++++++++++++++++++++++ src/gmser_dyn.erl | 20 +++++++----- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 doc/static.md diff --git a/README.md b/README.md index 4694d78..7f73e6c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Serialization helpers for the Gajumaru. +For an overview of the static serializer, see [this document](doc/static.md). + ## Build $ rebar3 compile @@ -30,6 +32,7 @@ how the type information is represented. The fully serialized form is produced by the `serialize` functions. The basic types supported by the encoder are: +* `integer()` (`anyint`, code: 246) * `neg_integer()` (`negint`, code: 247) * `non_neg_integer()` (`int` , code: 248) * `binary()` (`binary`, code: 249) @@ -40,6 +43,9 @@ The basic types supported by the encoder are: * `gmser_id:id()` (`id` , code: 254) * `atom()` (`label` , code: 255) +(The range of codes is chosen because the `gmser_chain_objects` codes +range from 10 to 200, and also to stay within 1 byte.) + When encoding `map` types, the map elements are first sorted. When specifying a map type for template-driven encoding, use @@ -122,8 +128,17 @@ to encode as each type in the list, in the specified order, until one matches. gmser_dyn:encode_typed(#{alt => [negint,int]}, 5) -> [<<0>>,<<1>>,[<<247>>,<<5>>]] gmser_dyn:encode_typed(#{alt => [negint,int]}, 5) -> [<<0>>,<<1>>,[<<248>>,<<5>>]] -gmser_dyn:register_type(246, anyint, #{alt => [negint, int]}) - gmser_dyn:encode_typed(anyint,-5) -> [<<0>>,<<1>>,[<<246>>,[<<247>>,<<5>>]]] gmser_dyn:encode_typed(anyint,5) -> [<<0>>,<<1>>,[<<246>>,[<<248>>,<<5>>]]] ``` + +### Notes + +Note that `anyint` is a standard type. The static serializer supports only +positive integers (`int`), as negative numbers are forbidden on-chain. +For dynamic encoding e.g. in messaging protocols, handling negative numbers can +be useful, so the `negint` type was added as a dynamic type. To encode a full-range +integer, the `alt` construct is needed. + +(Floats are not supported, as they are non-deterministic. Rationals and fixed-point +numbers could easily be handled as high-level types, e.g. as `{int,int}`.) diff --git a/doc/static.md b/doc/static.md new file mode 100644 index 0000000..991116b --- /dev/null +++ b/doc/static.md @@ -0,0 +1,83 @@ + +# Static Serialization + +The `gmserialization` and `gmser_chain_objects` modules implement the +static serialization support used in the Gajumaru blockchain. + +The purpose is to produce fully deterministic serialization, in order +to maintain predictable hashing. + +Example: + +```erlang +%% deterministic canonical serialization. +-spec serialize_to_binary(signed_tx()) -> binary_signed_tx(). +serialize_to_binary(#signed_tx{tx = Tx, signatures = Sigs}) -> + gmser_chain_objects:serialize( + ?SIG_TX_TYPE, + ?SIG_TX_VSN, + serialization_template(?SIG_TX_VSN), + [ {signatures, lists:sort(Sigs)} + , {transaction, aetx:serialize_to_binary(Tx)} + ]). + +-spec deserialize_from_binary(binary()) -> signed_tx(). +deserialize_from_binary(SignedTxBin) when is_binary(SignedTxBin) -> + [ {signatures, Sigs} + , {transaction, TxBin} + ] = gmser_chain_objects:deserialize( + ?SIG_TX_TYPE, + ?SIG_TX_VSN, + serialization_template(?SIG_TX_VSN), + SignedTxBin), + assert_sigs_size(Sigs), + #signed_tx{ tx = aetx:deserialize_from_binary(TxBin) + , signatures = Sigs + }. + +serialization_template(?SIG_TX_VSN) -> + [ {signatures, [binary]} + , {transaction, binary} + ]. +``` + +The terms that can be encoded using these templates are given by +this type in `gmserialization.erl`: + +```erlang +-type encodable_term() :: non_neg_integer() + | binary() + | boolean() + | [encodable_term()] %% Of any length + | #{atom() => encodable_term()} + | tuple() %% Any arity, containing encodable_term(). + | gmser_id:id(). +``` + +The template 'language' is defined by these types: + +```erlang +-type template() :: [{field_name(), type()}]. +-type field_name() :: atom(). +-type type() :: 'int' + | 'bool' + | 'binary' + | 'id' %% As defined in aec_id.erl + | [type()] %% Length one in the type. This means a list of any length. + | #{items := [{field_name(), type()}]} %% Record with named fields + %% represented as a map. + %% Encoded as a list in the given + %% order. + | tuple(). %% Any arity, containing type(). This means a static size array. +``` + +The `gmser_chain_objects.erl` module specifies a serialization code for each +object that can go on-chain. E.g.: + +```erlang +tag(signed_tx) -> 11; +... +rev_tag(11) -> signed_tx; +``` + +The `tag` and `vsn` are laid out in the beginning of the serialized object. diff --git a/src/gmser_dyn.erl b/src/gmser_dyn.erl index 69ff56b..c3b0713 100644 --- a/src/gmser_dyn.erl +++ b/src/gmser_dyn.erl @@ -86,7 +86,8 @@ decode_tag_and_vsn([TagBin, VsnBin, Fields]) -> dynamic_types() -> #{ vsn => ?VSN , codes => - #{ 247 => negint + #{ 246 => anyint + , 247 => negint , 248 => int , 249 => binary , 250 => bool @@ -96,7 +97,8 @@ dynamic_types() -> , 254 => id , 255 => label} , rev => - #{ negint => 247 + #{ anyint => 246 + , negint => 247 , int => 248 , binary => 249 , bool => 250 @@ -108,7 +110,8 @@ dynamic_types() -> , labels => #{} , rev_labels => #{} , templates => - #{ negint => negint + #{ anyint => #{alt => [negint, int]} + , negint => negint , int => int , binary => binary , bool => bool @@ -197,11 +200,6 @@ encode_typed_(Tag, Term, E, Vsn, #{templates := Ts, rev := Rev} = Types) encode_typed_(MaybeTemplate, Term, _, Vsn, Types) -> encode_maybe_template(MaybeTemplate, Term, Vsn, Types). -maybe_emit(E, Code, Enc) when E > 0 -> - [encode_basic(int, Code), Enc]; -maybe_emit(0, _, Enc) -> - Enc. - encode_maybe_template(#{items := _} = Type, Term, Vsn, Types) -> case is_map(Term) of true -> @@ -631,6 +629,7 @@ dynamic_types_test_() -> [ ?_test(revert_to_default_types()) , ?_test(t_typed_map()) , ?_test(t_alts()) + , ?_test(t_anyints()) ]. t_round_trip(T) -> @@ -734,4 +733,9 @@ t_alts() -> t_round_trip_typed(#{alt => [negint, int]}, 4), ok. +t_anyints() -> + t_round_trip_typed(anyint, -5), + t_round_trip_typed(anyint, 5), + ok. + -endif.