Proof-of-concept implementation of ASN.1 mapping #62
@@ -0,0 +1,225 @@
|
||||
-- GajumaruSerialization.asn
|
||||
--
|
||||
-- ASN.1 model for the structures serialized by gmserialization.erl
|
||||
-- (the layer on top of RLP).
|
||||
--
|
||||
-- Purpose:
|
||||
-- * Provide a formal, toolable description of the data.
|
||||
-- * Enable migration from the legacy RLP-based format to DER.
|
||||
--
|
||||
-- Detection of legacy vs new:
|
||||
-- Legacy data (produced by gmserialization:serialize / gmser_chain_objects:serialize
|
||||
-- or gmser_dyn:serialize) is always an RLP *list* at the top level.
|
||||
-- This means the first byte is in the range 0xC0 .. 0xFF.
|
||||
--
|
||||
-- DER-encoded data using the types below will start with 0x30 (SEQUENCE,
|
||||
-- constructed, universal tag) for the outermost GajumaruData (or any
|
||||
-- top-level SEQUENCE we define). 0x30 < 0xC0, so a single-byte prefix
|
||||
-- check reliably distinguishes the two formats.
|
||||
--
|
||||
-- Usage in Erlang (after compiling with asn1ct):
|
||||
-- {ok, Mod} = asn1ct:compile(GajumaruSerialization.asn, [ber, der]).
|
||||
-- {ok, Term} = 'GajumaruSerialization':decode('GajumaruData', DerBinary).
|
||||
--
|
||||
-- Notes on type mapping from gmserialization.erl:
|
||||
-- - int -> INTEGER (new data uses canonical DER INTEGER)
|
||||
-- - binary -> OCTET STRING
|
||||
-- - bool -> BOOLEAN
|
||||
-- - id -> Id (SEQUENCE) (cleaner than the legacy packed 33-byte form)
|
||||
-- - [T] -> SEQUENCE OF T
|
||||
-- - {T1,...} -> SEQUENCE (fixed-size, order matters)
|
||||
-- - #{items := [{K,T},...]} -> SEQUENCE with the fields in template order
|
||||
-- (static maps carry *values only*, no keys on the wire)
|
||||
-- - tag + vsn are preserved as the first two fields for compatibility
|
||||
-- with existing dispatch logic.
|
||||
--
|
||||
-- Legacy integers had strict unsigned-minimal no-leading-zero (except for 0)
|
||||
-- and RLP-level rules. New DER data does not need to follow those rules.
|
||||
--
|
||||
-- WARNING:
|
||||
-- Any cryptographic hash or signature computed over a serialized object
|
||||
-- will change when switching from RLP to DER for that object. Plan a
|
||||
-- coordinated upgrade.
|
||||
|
||||
GajumaruSerialization DEFINITIONS
|
||||
AUTOMATIC TAGS ::=
|
||||
BEGIN
|
||||
|
||||
EXPORTS ALL;
|
||||
|
||||
-- ============================================================
|
||||
-- Top-level wrapper used for new DER data.
|
||||
-- This is what a decoder will see first.
|
||||
-- ============================================================
|
||||
|
||||
GajumaruData ::= SEQUENCE {
|
||||
tag INTEGER,
|
||||
vsn INTEGER,
|
||||
content Content
|
||||
}
|
||||
|
||||
-- Content can be a specific structured type (preferred) or a generic
|
||||
-- representation of a template-driven object.
|
||||
Content ::= CHOICE {
|
||||
-- Generic fallback that can represent any [{Field, Type}] template
|
||||
-- without having a pre-defined SEQUENCE for every object.
|
||||
templateFields [0] TemplateFields,
|
||||
|
||||
-- Examples of concrete versioned types (extend as needed)
|
||||
account [1] Account,
|
||||
signedTx [2] SignedTx,
|
||||
contract [3] ContractCode
|
||||
-- Add more alternatives for other tags from gmser_chain_objects
|
||||
}
|
||||
|
||||
-- ============================================================
|
||||
-- Generic template-driven representation
|
||||
-- (useful during transition or for unregistered types)
|
||||
-- ============================================================
|
||||
|
||||
TemplateFields ::= SEQUENCE OF TemplateField
|
||||
|
||||
TemplateField ::= SEQUENCE {
|
||||
-- Field name is included for debuggability / generic processing.
|
||||
-- In the original static encoding the name is NOT on the wire;
|
||||
-- only position and type matter. We include it here for convenience.
|
||||
name IA5String OPTIONAL,
|
||||
value Value
|
||||
}
|
||||
|
||||
Value ::= CHOICE {
|
||||
intValue [0] INTEGER,
|
||||
boolValue [1] BOOLEAN,
|
||||
binaryValue [2] OCTET STRING,
|
||||
idValue [3] Id,
|
||||
listValue [4] SEQUENCE OF Value,
|
||||
tupleValue [5] SEQUENCE OF Value,
|
||||
mapValue [6] SEQUENCE OF KeyValue -- only needed if you want to
|
||||
-- represent dynamic-style maps
|
||||
}
|
||||
|
||||
KeyValue ::= SEQUENCE {
|
||||
key Value,
|
||||
val Value
|
||||
}
|
||||
|
||||
-- ============================================================
|
||||
-- Common types
|
||||
-- ============================================================
|
||||
|
||||
Id ::= SEQUENCE {
|
||||
-- Corresponds to the simple tags in gmser_id (account=1, name=2, etc.)
|
||||
-- and the extended account subtype (high bit in legacy).
|
||||
type INTEGER (0..255),
|
||||
value OCTET STRING (SIZE (32))
|
||||
}
|
||||
|
||||
-- ============================================================
|
||||
-- Concrete object examples (derived from usage in the codebase)
|
||||
-- Add / evolve per version as you introduce new vsns.
|
||||
-- ============================================================
|
||||
|
||||
-- Example: a very simple account-like object used in tests
|
||||
Account ::= SEQUENCE {
|
||||
foo INTEGER,
|
||||
bar OCTET STRING
|
||||
}
|
||||
|
||||
-- Simplified signed transaction
|
||||
SignedTx ::= SEQUENCE {
|
||||
signatures SEQUENCE OF OCTET STRING,
|
||||
transaction OCTET STRING
|
||||
}
|
||||
|
||||
-- Contract code objects (see gmser_contract_code.erl)
|
||||
-- We model the three versions that exist today.
|
||||
ContractCode ::= CHOICE {
|
||||
v1 [0] ContractV1,
|
||||
v2 [1] ContractV2,
|
||||
v3 [2] ContractV3
|
||||
}
|
||||
|
||||
ContractV1 ::= SEQUENCE {
|
||||
sourceHash OCTET STRING,
|
||||
-- typeInfo is a list of 4-tuples in legacy:
|
||||
-- {typeHash, name, argType, outType}
|
||||
typeInfo SEQUENCE OF TypeInfoV1,
|
||||
byteCode OCTET STRING
|
||||
}
|
||||
|
||||
ContractV2 ::= SEQUENCE {
|
||||
sourceHash OCTET STRING,
|
||||
typeInfo SEQUENCE OF TypeInfoV1,
|
||||
byteCode OCTET STRING,
|
||||
compilerVersion OCTET STRING
|
||||
}
|
||||
|
||||
ContractV3 ::= SEQUENCE {
|
||||
sourceHash OCTET STRING,
|
||||
typeInfo SEQUENCE OF TypeInfoV3,
|
||||
byteCode OCTET STRING,
|
||||
compilerVersion OCTET STRING,
|
||||
payable BOOLEAN
|
||||
}
|
||||
|
||||
TypeInfoV1 ::= SEQUENCE {
|
||||
typeHash OCTET STRING,
|
||||
name OCTET STRING,
|
||||
argType OCTET STRING,
|
||||
outType OCTET STRING
|
||||
}
|
||||
|
||||
TypeInfoV3 ::= SEQUENCE {
|
||||
typeHash OCTET STRING,
|
||||
name OCTET STRING,
|
||||
payable BOOLEAN,
|
||||
argType OCTET STRING,
|
||||
outType OCTET STRING
|
||||
}
|
||||
|
||||
-- ============================================================
|
||||
-- Notes for implementers
|
||||
-- ============================================================
|
||||
-- 1. Detection (recommended decoder entry point):
|
||||
--
|
||||
-- decode(Binary) ->
|
||||
-- case Binary of
|
||||
-- <<B, _/binary>> when B >= 16#C0 ->
|
||||
-- decode_legacy_rlp(Binary); % existing gmser_* path
|
||||
-- _ ->
|
||||
-- {ok, Term} =
|
||||
-- 'GajumaruSerialization':decode('GajumaruData', Binary),
|
||||
-- Term
|
||||
-- end.
|
||||
--
|
||||
-- This works because:
|
||||
-- - All current top-level output from serialize() is an RLP list
|
||||
-- (first byte 0xC0-0xFF).
|
||||
-- - GajumaruData and the concrete choices above are SEQUENCEs
|
||||
-- (first byte 0x30 for short form, or 0x30 0x81/0x82... for long).
|
||||
--
|
||||
-- 2. When you add a new object type or version, prefer adding a
|
||||
-- concrete SEQUENCE alternative in the Content CHOICE rather than
|
||||
-- always falling back to TemplateFields. This gives you better
|
||||
-- validation and generated types.
|
||||
--
|
||||
-- 3. INTEGER in DER is signed and uses a different minimal encoding
|
||||
-- than the legacy unsigned big-endian no leading zero form. This is
|
||||
-- fine for new data.
|
||||
--
|
||||
-- 4. If you need to preserve exact legacy integer wire bytes inside
|
||||
-- the new format (e.g. for some hash preimage reason), you can
|
||||
-- carry selected integers as OCTET STRING (legacy unsigned minimal bytes).
|
||||
--
|
||||
-- 5. Dynamic encoding (gmser_dyn) is intentionally not modeled here
|
||||
-- in full, because it is runtime-schema driven (type codes 246-255,
|
||||
-- labels, alt/switch, etc.). You can still use the generic
|
||||
-- TemplateFields + Value for some dynamic cases, or model specific
|
||||
-- message schemas as additional CHOICE arms.
|
||||
--
|
||||
-- 6. Compile with DER for canonical output:
|
||||
-- asn1ct:compile(GajumaruSerialization, [der]).
|
||||
--
|
||||
-- The ber option is also accepted; der implies the stricter rules.
|
||||
|
||||
END
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
%% Generated by the Erlang ASN.1 compiler. Version: 5.4.3
|
||||
%% Purpose: Erlang record definitions for each named and unnamed
|
||||
%% SEQUENCE and SET, and macro definitions for each value
|
||||
%% definition in module GajumaruSerialization.
|
||||
|
||||
-ifndef(_GAJUMARUSERIALIZATION_HRL_).
|
||||
-define(_GAJUMARUSERIALIZATION_HRL_, true).
|
||||
|
||||
-record('GajumaruData', {
|
||||
tag,
|
||||
vsn,
|
||||
content
|
||||
}).
|
||||
|
||||
-record('TemplateField', {
|
||||
name = asn1_NOVALUE,
|
||||
value
|
||||
}).
|
||||
|
||||
-record('KeyValue', {
|
||||
key,
|
||||
val
|
||||
}).
|
||||
|
||||
-record('Id', {
|
||||
type,
|
||||
value
|
||||
}).
|
||||
|
||||
-record('Account', {
|
||||
foo,
|
||||
bar
|
||||
}).
|
||||
|
||||
-record('SignedTx', {
|
||||
signatures,
|
||||
transaction
|
||||
}).
|
||||
|
||||
-record('ContractV1', {
|
||||
sourceHash,
|
||||
typeInfo,
|
||||
byteCode
|
||||
}).
|
||||
|
||||
-record('ContractV2', {
|
||||
sourceHash,
|
||||
typeInfo,
|
||||
byteCode,
|
||||
compilerVersion
|
||||
}).
|
||||
|
||||
-record('ContractV3', {
|
||||
sourceHash,
|
||||
typeInfo,
|
||||
byteCode,
|
||||
compilerVersion,
|
||||
payable
|
||||
}).
|
||||
|
||||
-record('TypeInfoV1', {
|
||||
typeHash,
|
||||
name,
|
||||
argType,
|
||||
outType
|
||||
}).
|
||||
|
||||
-record('TypeInfoV3', {
|
||||
typeHash,
|
||||
name,
|
||||
payable,
|
||||
argType,
|
||||
outType
|
||||
}).
|
||||
|
||||
-endif. %% _GAJUMARUSERIALIZATION_HRL_
|
||||
@@ -0,0 +1,27 @@
|
||||
-module(detect_demo).
|
||||
-export([run/0]).
|
||||
|
||||
run() ->
|
||||
application:ensure_all_started(gmserialization),
|
||||
code:add_path("asn1"),
|
||||
|
||||
Sample = {'GajumaruData', 10, 1,
|
||||
{templateFields, [
|
||||
{'TemplateField', <<"foo">>, {intValue, 42}},
|
||||
{'TemplateField', <<"bar">>, {binaryValue, <<"hello">>}}
|
||||
]}},
|
||||
|
||||
{ok, Der} = 'GajumaruSerialization':encode('GajumaruData', Sample),
|
||||
io:format("DER first byte: ~p (0x~2.16.0B)~n", [binary:at(Der,0), binary:at(Der,0)]),
|
||||
|
||||
{ok, _Dec} = 'GajumaruSerialization':decode('GajumaruData', Der),
|
||||
io:format("DER roundtrip OK~n"),
|
||||
|
||||
Legacy = gmser_chain_objects:serialize(account, 1,
|
||||
[{foo,int},{bar,binary}],
|
||||
[{foo,42},{bar,<<"hello">>}]),
|
||||
<<L0>> = binary:part(Legacy,0,1),
|
||||
<<D0>> = binary:part(Der,0,1),
|
||||
io:format("Legacy 0x~2.16.0B (>= 0xC0 -> legacy: ~p)~n", [L0, L0 >= 16#C0]),
|
||||
io:format("DER 0x~2.16.0B (< 0xC0 -> new DER: ~p)~n", [D0, D0 < 16#C0]),
|
||||
ok.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user