-- 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 for portability -- (other languages use ASN.1 compilers to get types/parsers). -- * Define compact canonical wire format using standard ASN.1 techniques -- (primarily Unaligned PER / UPER with constraints for packing). -- * Static encoding only (templates from gmserialization). -- * Legacy RLP compatibility is achieved via separate translation layer -- (deferred in current focus). -- -- 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 for compact wire): -- {ok, Mod} = asn1ct:compile(GajumaruSerialization.asn, [uper]). -- {ok, Bytes} = 'GajumaruSerialization':encode('GajumaruData', Asn1Value). -- Bytes is the compact UPER wire format (deterministic with this schema). -- For legacy RLP, use the model-to-RLP layer instead (see gmser_asn1_rlp). -- -- 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 { -- Constrained for better PER packing tag INTEGER (0..65535), vsn INTEGER (0..255), content Content } -- Preferred top-level for the compact static wire format. -- Avoids the extra Content CHOICE tag when the structure is known to be static. CompactStatic ::= SEQUENCE { tag INTEGER (0..65535), vsn INTEGER (0..255), fields StaticFields } -- 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, -- Static-optimized: no field names on wire (matches legacy static behavior exactly) -- Preferred for compact wire format of known templates. staticFields [10] StaticFields, -- 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 } -- Optimized for static wire format: just the values in order, no names. -- This matches the legacy static encoding where maps/records are positional only. StaticFields ::= SEQUENCE OF 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 -- <> 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. For the compact wire format we use UPER (unaligned PER). -- It is stable/deterministic for a given schema (no extensibility -- markers on these types, fixed order). -- -- 4. INTEGER uses ASN.1 PER encoding (canonical for the constraints). -- This may differ from legacy RLP minimal unsigned; new format -- will have different hashes (expected when introducing new encoding). -- -- 5. Dynamic encoding (gmser_dyn) is not in scope here. -- -- 6. To generate the compact wire: -- asn1ct:compile(GajumaruSerialization, [uper]). -- {ok, CompactBytes} = 'GajumaruSerialization':encode('GajumaruData', Value). -- -- Use staticFields (not templateFields) for best compactness on static data. END