PoC: ASN.1 / DER serialization experiment
Gajumaru Serialization Tests / tests (push) Successful in 10s

This branch contains a proof-of-concept exploration of representing
the RLP (gmser_rlp) and gmserialization layers using ASN.1 with
DER encoding.

Goals of the experiment:
- Model the static serialization templates and dynamic types in ASN.1.
- Demonstrate reliable detection of legacy RLP data vs. new DER data
  (first byte >= 0xC0 for RLP lists vs. 0x30 for DER SEQUENCE).
- Evaluate wire-size overhead of standard DER TLV encoding compared
  to the extremely compact RLP prefix encoding.
- Verify that the DER path produces deterministic output suitable
  for hashing (idempotent serialization).

Key files:
- GajumaruSerialization.asn   -- the ASN.1 schema
- GajumaruSerialization.{erl,hrl} -- generated encoder/decoder
- detect_demo.erl, size_comparison.erl -- supporting test code

Findings (high level):
- Detection via first byte works cleanly.
- DER is fully deterministic for hashing when the same value is given.
- Small objects pay a significant size overhead (often 3-6x) due to
  TLV tags/lengths and field names in the generic path.
- Concrete SEQUENCE definitions are much more compact than the
  generic templateFields representation.
- Large payloads have acceptable relative overhead.

This is NOT production code. It was created to answer feasibility
questions around ASN.1 modeling, format detection, determinism,
and size characteristics.

Branch created from uw-new-acs for isolated experimentation.
This commit is contained in:
Ulf Wiger
2026-07-07 10:05:30 +02:00
parent c80505c810
commit 6d7ab1e4ad
5 changed files with 1844 additions and 0 deletions
+114
View File
@@ -0,0 +1,114 @@
-module(size_comparison).
-export([run/0]).
run() ->
application:ensure_all_started(gmserialization),
code:add_path("asn1"),
io:format("=== Size Comparison: Legacy RLP vs DER ===~n~n"),
Compare = fun(Desc, LegacyBin, DerBin) ->
L = byte_size(LegacyBin),
D = byte_size(DerBin),
Overhead = D - L,
Pct = case L of 0 -> 0; _ -> round(Overhead * 100 / L) end,
io:format("~s~n", [Desc]),
io:format(" Legacy: ~p bytes ~w~n", [L, LegacyBin]),
Show = binary:part(DerBin, 0, min(18, D)),
io:format(" DER: ~p bytes ~w~n", [D, Show]),
io:format(" Overhead: +~p bytes (~p%)~n~n", [Overhead, Pct])
end,
%% Case 1: tag+vsn + small int + small binary
T1 = [{foo,int},{bar,binary}],
V1 = [{foo,1},{bar,<<2>>}],
Leg1 = gmser_chain_objects:serialize(account, 1, T1, V1),
DerVal1 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"foo">>, {intValue, 1}},
{'TemplateField', <<"bar">>, {binaryValue, <<2>>}}
]}},
{ok, Der1} = 'GajumaruSerialization':encode('GajumaruData', DerVal1),
Compare("Case 1: tag+vsn + small int + tiny binary (2 fields)", Leg1, Der1),
%% Case 2: zero + empty binary
V2 = [{foo,0},{bar,<<>>}],
Leg2 = gmser_chain_objects:serialize(account, 1, T1, V2),
DerVal2 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"foo">>, {intValue, 0}},
{'TemplateField', <<"bar">>, {binaryValue, <<>>}}
]}},
{ok, Der2} = 'GajumaruSerialization':encode('GajumaruData', DerVal2),
Compare("Case 2: zero int + empty binary", Leg2, Der2),
%% Case 3: list of ints
T3 = [{xs,[int]}],
V3 = [{xs,[1,2,3]}],
Leg3 = gmser_chain_objects:serialize(account, 1, T3, V3),
DerVal3 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"xs">>, {listValue, [
{intValue,1},{intValue,2},{intValue,3}
]}}
]}},
{ok, Der3} = 'GajumaruSerialization':encode('GajumaruData', DerVal3),
Compare("Case 3: list of 3 small ints", Leg3, Der3),
%% Case 4: tuple (int, binary)
T4 = [{p,{int,binary}}],
V4 = [{p,{42,<<"hi">>}}],
Leg4 = gmser_chain_objects:serialize(account, 1, T4, V4),
DerVal4 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"p">>, {tupleValue, [
{intValue,42}, {binaryValue,<<"hi">>}
]}}
]}},
{ok, Der4} = 'GajumaruSerialization':encode('GajumaruData', DerVal4),
Compare("Case 4: fixed tuple (int + 2-byte binary)", Leg4, Der4),
%% Case 5: 256-byte payload
Bin5 = crypto:strong_rand_bytes(256),
T5 = [{data,binary}],
V5 = [{data,Bin5}],
Leg5 = gmser_chain_objects:serialize(account, 1, T5, V5),
DerVal5 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"data">>, {binaryValue, Bin5}}
]}},
{ok, Der5} = 'GajumaruSerialization':encode('GajumaruData', DerVal5),
Compare("Case 5: 256-byte binary payload only", Leg5, Der5),
%% Case 6: Concrete SignedTx style (no field names in DER)
T6 = [{signatures,[binary]},{tx,binary}],
V6 = [{signatures,[<<"sig1">>,<<"sig2">>]},{tx,<<"txbody123">>}],
Leg6 = gmser_chain_objects:serialize(signed_tx, 1, T6, V6),
DerVal6 = {'GajumaruData', 11, 1, {signedTx, {'SignedTx',
[<<"sig1">>, <<"sig2">>], <<"txbody123">>}}},
{ok, Der6} = 'GajumaruSerialization':encode('GajumaruData', DerVal6),
Compare("Case 6: SignedTx-like (concrete DER, no names)", Leg6, Der6),
%% Case 7: 33-byte id-like value
IdBin = <<1, 0:256>>,
T7 = [{owner,binary}],
V7 = [{owner,IdBin}],
Leg7 = gmser_chain_objects:serialize(account, 1, T7, V7),
DerVal7 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"owner">>, {binaryValue, IdBin}}
]}},
{ok, Der7} = 'GajumaruSerialization':encode('GajumaruData', DerVal7),
Compare("Case 7: 33-byte value (id-like)", Leg7, Der7),
%% Case 8: Deeper nesting / more fields
T8 = [{a,int},{b,binary},{c,[int]},{d,{int,int}}],
V8 = [{a,123456},{b,<<"abcdef">>},{c,[10,20,30]},{d,{7,8}}],
Leg8 = gmser_chain_objects:serialize(account, 1, T8, V8),
DerVal8 = {'GajumaruData', 10, 1, {templateFields, [
{'TemplateField', <<"a">>, {intValue, 123456}},
{'TemplateField', <<"b">>, {binaryValue, <<"abcdef">>}},
{'TemplateField', <<"c">>, {listValue, [{intValue,10},{intValue,20},{intValue,30}]}},
{'TemplateField', <<"d">>, {tupleValue, [{intValue,7},{intValue,8}]}}
]}},
{ok, Der8} = 'GajumaruSerialization':encode('GajumaruData', DerVal8),
Compare("Case 8: 4 fields mixed (int, bin, list, tuple)", Leg8, Der8),
io:format("=== Analysis ===~n"),
io:format("Note: Generic templateFields path includes IA5String field names.~n"),
io:format("Concrete types (e.g. SignedTx) avoid name overhead.~n"),
ok.