Skip to content

feat(e2sm-dapp): add the E2SM-DAPP grammar, codec and specification - #124

Open
Thecave3 wants to merge 11 commits into
mainfrom
feat/e2sm-dapp-codec
Open

Thecave3 wants to merge 11 commits into
mainfrom
feat/e2sm-dapp-codec

Conversation

@Thecave3

@Thecave3 Thecave3 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • libe3 becomes the home of the E2SM-DAPP service model (RAN function 255, OID 1.3.6.1.4.1.53148.1.1.255.3). Until now the grammar and its meaning lived only inside one RAN stack's source tree, so no other stack could encode or decode these messages.
  • Anyone can now link a small APER codec for all eight E2SM-DAPP messages, from C++ or from C, with no E3 agent, ZeroMQ or E3AP grammar pulled in. The grammar is installed, so a stack that prefers to run asn1c itself can do that instead.
  • docs/e2sm-dapp/ is the specification, in six parts, plus a separate integrator guide.

What changes for a user

  • New: find_package(libe3 REQUIRED COMPONENTS e2sm_dapp) gives libe3::e2sm_dapp (static) and libe3::e2sm_dapp_shared. Headers: libe3/e2sm_dapp.hpp and libe3/e2sm_dapp_c.h. The grammar goes to share/libe3/e2sm_dapp/.
  • Build flag: LIBE3_ENABLE_E2SM_DAPP, default ON, needs LIBE3_ENABLE_ASN1. With ASN.1 off it is skipped with a status message, not an error.
  • Unchanged: the E3 wire protocol, the E3 agent and the existing libe3 library. The main library does not link the new one.
  • Not included: the codec for the Spectrum service model's inner payload (the control outcome carries it as opaque bytes; the docs describe it). There is no E2AP or E2 agent code here, only the service model's information elements.

How it is checked

  • The expected bytes (tests/e2sm_dapp_golden.hpp) are APER encodings of 31 inputs produced by an existing, independent E2SM-DAPP encoder. Every one must pack to those exact bytes and unpack to an equal struct. tools/e2sm_dapp_golden/regen.sh reproduces the header from a pristine checkout of that encoder.
  • Encoding and decoding have their own test files, with round-trip, C API and link-coexistence tests next to them. The decoder is run over every truncated prefix of every vector and over random and corrupted input, under ASan and UBSan.
  • The C++ type and field names deliberately mirror a second, independently written implementation of the same grammar, so call sites read the same in both. tests/e2sm_dapp_fixtures.inc builds every test input through those shared names.

Things to know

  • libe3's codec is stricter than the encoder the vectors come from. It checks every ASN.1 constraint before encoding and returns an error instead of aborting; it also has no fixed encode buffer, so a 32768 byte control message works.
  • Both libe3 and libe3::e2sm_dapp carry an asn1c runtime. The one in the codec is hidden, and a test links and runs both together.
  • VERSION goes to 0.3.0 (new public API).
  • libe3.pc gains -DLIBE3_ENABLE_E2SM_DAPP in its Cflags, and a separate libe3_e2sm_dapp.pc is installed.

Type of change

  • New feature / enhancement
  • Documentation
  • Test / CI / packaging

Linked issue

No issue exists for this yet. I can file one if you want it linked.

Test checklist

  • ./build_libe3 -c -d build -j $(nproc) -r -t (Release): not run through the script. The E2SM-DAPP tests pass in a Release build; CI runs the full set.
  • ./build_libe3 -c -d build -j $(nproc) -g -t (Debug): the equivalent plain CMake Debug build passes 30 of 30 tests on macOS arm64, with ZeroMQ on and JSON and protobuf off.
  • cd build && ctest --output-on-failure: see above. Also built with the flag OFF (20 of 20) and with ASN.1 OFF (19 of 19).
  • MPMC queue benchmark: not run; mpmc_queue.hpp is untouched.
  • VERSION bumped (0.2.2 to 0.3.0).
  • ./build_libe3 --docs: not run. Public headers carry Doxygen comments.
  • No new build dependencies.
  • libe3.pc consumers: the Cflags change above is additive. I could not check dApp-openairinterface5g.

Twin-repo coordination

  • This PR does not change the E3 wire protocol or the public ABI of existing symbols (additive only).

Workflow confirmation

  • Linear history on top of main, no merge commits.
  • Every commit builds and passes tests on its own: the first three were each built in a separate worktree; the Commit policy job covers the rest.
  • I have read and followed CONTRIBUTING.md.

…DAPP

Add the E2SM-DAPP grammar (a byte-identical copy of flexric's
e2sm_dapp_v0_standard.asn, so the two can be compared with cmp) and a second,
independent asn1c run for it, with the same flags as the E3AP one.

The output goes to its own directory and is built into its own OBJECT library,
asn1_e2sm_dapp, with a copy of the asn1c runtime of its own, compiled -fPIC,
-fvisibility=hidden and -w. It shares nothing with asn1_e3ap and does not touch
it. An OBJECT library so that the codec libraries that follow can embed the
objects and no separate asn1c archive has to be installed.

The new option LIBE3_ENABLE_E2SM_DAPP defaults to ON. It needs
LIBE3_ENABLE_ASN1: with ASN.1 off, configure prints a status message and
treats the codec as off instead of failing. The grammar has no room for an
inline SPDX header, so REUSE.toml annotates it.

Assisted-by: Claude:claude-sonnet-5-5
Add libe3_e2sm_dapp (static, alias libe3::e2sm_dapp) and libe3_e2sm_dapp_shared
(alias libe3::e2sm_dapp_shared), with the C++ API of include/libe3/e2sm_dapp.hpp
and the C API of include/libe3/e2sm_dapp_c.h. Both embed the asn1_e2sm_dapp
objects ($<TARGET_OBJECTS>) and depend on nothing else in libe3: no agent, no
ZeroMQ, no tl::expected, no E3AP grammar. The main libe3 targets do not link
them; they only gain a LIBE3_ENABLE_E2SM_DAPP feature define, which reaches
libe3.pc like the other feature macros.

Encoding builds the asn1c struct, checks every constraint of the grammar by
hand first (asn1c can fault inside the encoder on an oversized payload, and
silently uses its extension encoding for an out-of-range extensible integer),
runs asn_check_constraints as a second net, and encodes APER aligned into a
growing buffer, so the 32768 byte maximum of a control message fits. Anything
outside the grammar is ASN_ERROR_ENCODE_FAIL and leaves the output empty.
Decoding requires RC_OK and applies the same constraints, so a decoded struct
always packs again; trailing bytes are accepted, an empty or null buffer, a
truncated one and an unknown CHOICE alternative are ASN_ERROR_DECODE_FAIL.
The asn1c struct is freed on every path. The extension flag is ignored on
encode and false after decode: no extension additions are defined, and asn1c
skips unknown ones.

The C API is implemented on top of the C++ one and maps asn_code to e3_error_t.
Everything it returns is malloc'ed, so the free_* functions and plain free both
release it, and free_* zero the struct.

The shared library exports only libe3::e2sm_dapp::* and libe3_e2sm_dapp_*
(a version script on Linux, an exported symbols list on macOS); the asn1c
runtime inside it is hidden, so a consumer with an asn1c of its own does not
clash with it.

Assisted-by: Claude:claude-sonnet-5-5
Install the two public headers, the grammar (to share/libe3/e2sm_dapp/) and both
libraries, and put the libraries in the exported set under the names
libe3::e2sm_dapp and libe3::e2sm_dapp_shared (EXPORT_NAME, so they are not
libe3::libe3_e2sm_dapp). e2sm_dapp.hpp is excluded from the blanket header
install, so a build without the codec does not install a header whose symbols
are not there. A separate libe3_e2sm_dapp.pc carries the library, the include
path and the feature define for pkg-config consumers.

find_package(libe3 REQUIRED COMPONENTS e2sm_dapp) now works: the component is
found exactly when the install has both targets, and a required component that
is missing (or unknown) fails with a message that says why. Without COMPONENTS
nothing changes.

The generated asn1c headers of the codec are private and live outside the
messages/ build directory, whose *.h are all installed for the E3AP grammar; the
CMakeFiles directory of that tree is no longer installed as empty directories.

tests/consume checks the installed package: the existing executable also packs
and unpacks one message through libe3::e2sm_dapp next to the E3 agent, and a new
consume_e2sm_dapp (static and shared) links the codec alone, from C++ and from C,
on both the CMake and the pkg-config route. LIBE3_CONSUME_E2SM_DAPP=AUTO|ON|OFF
selects whether the codec is required.

Assisted-by: Claude:claude-sonnet-5-5
tests/e2sm_dapp_golden.hpp holds the APER bytes flexric's own encoder produces
for 31 inputs (hex for the small ones, length and FNV-1a 64 for the four that
are too large to inline), and tests/e2sm_dapp_fixtures.inc builds the same
inputs in C++ against the names libe3 and OCUDU share (a byte-identical copy
lives in OCUDU). The vectors are the byte-compatibility contract: libe3 must
reproduce each one exactly and decode it back to an equal struct.

  test_e2sm_dapp_encode     every fixture packs to its golden vector; optional
                            fields; boundary values; every value outside the
                            grammar is ASN_ERROR_ENCODE_FAIL with `out` empty
  test_e2sm_dapp_decode     golden bytes unpack to the fixture; every proper
                            prefix of every vector is rejected (none is a valid
                            shorter PDU); empty and null input; trailing bytes;
                            extension bit; deterministic fuzz, which also
                            checks that whatever decodes packs again
  test_e2sm_dapp_roundtrip  pack, unpack, compare, re-pack, including the
                            32768 byte control message that has no golden vector
  test_e2sm_dapp_c_api      the C API on every message type: golden bytes, NULL
                            arguments, double free, error codes, plain free
  test_e2sm_dapp_link_coexist
                            libe3 and libe3::e2sm_dapp, each with its own asn1c
                            runtime, in one executable: static + static, and
                            shared libe3 + static codec

The first four are built against both the static and the shared codec library.
They link libe3::e2sm_dapp alone, and are skipped when the codec is not built.

Assisted-by: Claude:claude-sonnet-5-5
regen.sh <flexric-checkout> <asn1c-binary> <gcc> [output-file] rebuilds
tests/e2sm_dapp_golden.hpp from an unmodified flexric checkout: it copies the
parts of dapp_sm that the codec needs into a scratch tree, drops four unused
includes that pull in other service models, regenerates the asn1c code from
flexric's grammar, compiles gen_golden.c against flexric's real encoder and
decoder with a GCC (flexric's defer macro needs nested functions), and runs
gen_header.py on the output. The flexric commit named in the header comes from
git rev-parse HEAD of the checkout. Run against flexric 71631466 it reproduces
the committed header byte for byte.

gen_golden.c and gen_header.py, which produced the vectors, join the tree here.
gen_golden.c gets its SPDX header, and gen_header.py wraps the SPDX lines it
prints in REUSE-IgnoreStart/End so that reuse lint does not read them as its
own; its output is unchanged.

Assisted-by: Claude:claude-sonnet-5-5
Specify E2SM-DAPP sections 1 to 3: RAN function id, OID and how the
definition is announced in E2 Setup and RIC Service Update; every
information element with fields, ranges, formats and the service to
format table, including the control outcome payload and the Spectrum
worked example; and the discovery, subscription, indication and
deferred-acknowledge flow with the sequence-id chain and the code of
each step in flexric, OAI, xDevSM and OCUDU.

Assisted-by: Claude:claude-sonnet-5-5
Specify E2SM-DAPP sections 4 to 6: the APER rules with byte-level worked
examples re-derived against the golden vectors, size limits, the type map
across flexric, libe3 and OCUDU and the build switches; units, clocks,
absent versus zero, validation and error paths per implementation; and
the list of discrepancies, limits, aborts and dead code found in the
code, with file and line.

Assisted-by: Claude:claude-sonnet-5-5
Add the dispatcher page that maps the six sections to their files, the
integrator guide for a RAN stack adopting the codec (link libe3, run
asn1c, the OCUDU to libe3 interface, the shared fixtures, the golden
vector contract and a checklist), and link the specification from the
top-level README and its directory tree.

Assisted-by: Claude:claude-sonnet-5-5
…bles

The E2SM-DAPP codec is a new public API, so the next free minor is 0.3.0.

Assisted-by: Claude:claude-sonnet-5-5
The generator's offset basis had its last digit dropped, so the hashes of the
four vectors too large to inline were not FNV-1a 64 values, although the
documentation said they were. Fix the constant in the generator and the test
helper, regenerate the header, and update the hashes in the encoding notes.
The bytes of every vector are unchanged.

Assisted-by: Claude:claude-sonnet-5-5
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

CI report — 5703c81 — ❌ 1 required check failed

Workflow Result Time Run
E2E Topologies (multi-dApp / multi-RAN) ❌ failure (Topologies (zmq/tcp)) 5m36s #135
Commit policy ✅ success 11m15s #132
E2E dApp Integration ✅ success 8m20s #137
Full-loop Latency Benchmark ✅ success 1m24s #136
Unit Tests ✅ success 8m52s #163
latrec portability ⏭️ not triggered (paths filter) — —
MPMC Queue Benchmark ⏭️ not triggered (paths filter) — —

Failing required checks: Topologies (zmq/tcp)

E2E Topologies (multi-dApp / multi-RAN)

zmq/ipc

  • ✅ 1 RAN - 1 dApp: indications=5
    • dapp peer=t11 ran=ran-solo sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0.8 max=1 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
  • ✅ 1 RAN - 2 dApps: dApp#1 ind=5 sub=1, dApp#2 ind=6 sub=2, RAN saw 2 dApps
    • dapp1 peer=t12 ran=ran-shared sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp2 peer=t12 ran=ran-shared sub=2 indications=6 seq=[0..5] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:6 2-5:0 6-10:0 >10:0]
  • ✅ 2 RANs - 1 dApp: from ran-a ind=5, from ran-b ind=5
    • dapp peer=t2a ran=ran-a sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
    • dapp peer=t2b ran=ran-b sub=1 indications=5 seq=[0..4] dropped=0 (0%) age_ms(avg=0 max=0 @seq=0) hist[<=1:5 2-5:0 6-10:0 >10:0]
E2E dApp Integration

✅ posix/ipc

  • dApp exit: 0
  • Indications received: 7

✅ posix/tcp

  • dApp exit: 0
  • Indications received: 7

✅ zmq/ipc

  • dApp exit: 0
  • Indications received: 7

✅ zmq/tcp

  • dApp exit: 0
  • Indications received: 7
Full-loop Latency Benchmark

Full-loop latency

Full-loop latency benchmark (N=1021 after 50 warmup)

All values in microseconds (μs). Link: zmq, transport: ipc, encoding: ASN.1 APER.

# Description Tags mean p50 p99 max
1 Collect indication data RECORD_BEGIN to ENCODE_E3SM_BEGIN 0.12 0.11 0.35 2.13
2 Create & encode indication ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 0.99 1.00 1.51 3.68
3 Encode E3AP (indication) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 3.02 3.02 5.52 5.86
4 Queuing (indication) ENQUEUE to DEQUEUE 4.44 3.57 12.02 77.52
5 Delivery (indication) ENCODE_E3AP_DONE to SEND_DONE 0.27 0.25 0.70 1.86
6 E3 wire (RAN -> dApp) SEND_DONE to RECV 52.11 53.17 63.37 175.40
7 Decode E3AP (indication) RECV to DECODE_E3AP_DONE 1.62 1.45 2.58 18.50
8 libe3 dispatch (indication) DECODE_E3AP_DONE to DELIVER_BEGIN 0.10 0.10 0.16 0.27
9 Decode indication DELIVER_BEGIN to DECODE_E3SM_DONE 0.55 0.52 0.83 1.09
10 Process data DECODE_E3SM_DONE to ENCODE_E3SM_BEGIN 0.04 0.04 0.05 0.16
11 Create & encode control ENCODE_E3SM_BEGIN to ENCODE_E3SM_DONE 0.35 0.35 0.50 0.64
12 Encode E3AP (control) EMIT_ENTER to ENQUEUE, then DEQUEUE to ENCODE_E3AP_DONE 3.90 3.92 4.83 6.61
13 Queuing (control) ENQUEUE to DEQUEUE 17.81 17.09 24.20 77.88
14 Delivery (control) ENCODE_E3AP_DONE to SEND_DONE 5.49 5.21 8.24 11.11
15 E3 wire (dApp -> RAN) SEND_DONE to RECV 51.01 51.46 59.91 304.75
16 Decode E3AP (control) RECV to DECODE_E3AP_DONE 2.87 2.79 4.20 10.86
17 libe3 dispatch (control) DECODE_E3AP_DONE to DECODE_E3SM_BEGIN 0.30 0.30 0.42 0.50
18 Decode & handle control DECODE_E3SM_BEGIN to DECODE_E3SM_DONE 0.41 0.38 0.57 0.79
Total Total round-trip 145.86 146.95 166.50 603.06

ubuntu-latest, Release build, ZMQ + IPC, ASN.1 APER.

These numbers are measured inside a GitHub Actions container and should be treated as an upper bound on E3AP's and the library's own latency, not a representative deployment measurement.

One comment per PR, rewritten in place once every workflow for 5703c81 finished.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant