Expand description
DSig as the signature service of a uBFT
replica: the same slices, certificate unit and verify core as the batch
kernels, behind four AXI4-Stream ports, so a consensus engine on the same
U280 can sign and verify in microseconds instead of milliseconds. Built
with the ubft feature; the hardware is crate::ubft.
In numbers (October 2026; every row says what kind of evidence it is):
| uBFT today (inline xf Ed25519) | With the DSig service | Evidence | |
|---|---|---|---|
| Sign one share | 3,071,249 cycles (19.0 ms projected at the estimated 161.3 MHz) | 207–217 cycles = 1.01–1.06 µs at 205 MHz through the whole service kernel (the signer alone: 171–188); profile 2: 207–257 | HLS C/RTL cosim of uBFT’s own wrapper (reproduced, crates/dsig/measure/ubft-baseline); native RHDL simulation (tests/ubft_engine.rs; tests/ubft_signer.rs) |
| Verify one share | 10,411,579 cycles (64.5 ms projected) | 731–737 cycles = 3.6 µs (certificate cached); +52,670 on a miss; profile 2: 766–769 | HLS cosim; native RHDL simulation (tests/ubft_blocks.rs, instant Ed25519 model) + the Ed25519 project’s measured core latency |
| uBFT kernel itself | 234,341 LUT, 4.68 ns (misses 300 MHz) | 37,971 LUT, 3.12 ns (meets 300 MHz) | HLS synthesis of ubft with scheme 0 (inline) and scheme 1 (DSig streams) |
| Clock uBFT’s design closes at | 161.3 MHz (its signer is the worst path, −2.866 ns) | not measured yet | uBFT’s routed link log |
| The service on the card | — | building (UbftCard, link in progress) | — |
So a slow-path slot (one signature, 2f verifications) drops from hundreds
of milliseconds to a few microseconds of crypto, and the consensus kernel
loses 84 % of its LUTs and its timing problem.
§The story in five minutes
The job. uBFT signs a summary of each slot and checks its peers’ signatures until it holds a quorum. It does that with the Vitis Security Library’s Ed25519, called inline: the replica’s state machine stops for 19 ms per signature and 64.5 ms per verification, and that signer is so deep that Vitis pulled the whole design’s clock from 300 to 161.3 MHz.
The first idea was to hand uBFT the batch kernels. They are fast, but they run from HBM, start from the host and finish once; a consensus engine needs an answer to one request at a time, now.
So, one idea per step: put the batch engine’s slices behind a request
stream (a dealer gives each request to the first idle slice; keys are made
ahead, so a request never waits for key generation); put a verify core
behind a second stream with an in-order tag FIFO; let the Ed25519 verify
core hold one trusted key per peer (8 slots, N ≤ 9).
What broke was an assumption in the certificate cache: with one trusted
key it was enough to match the 128-byte certificate, but with many a
certificate checked for replica A would have answered for replica B. Every
cache entry now remembers its key slot (crate::verify::certs).
Where it stands. The service signs and verifies in simulation at 1.01–1.06 µs (whole kernel) and 3.6 µs; uBFT’s code calls it over streams (a local, unpushed branch of ubft_new) and synthesizes 84 % smaller at 300 MHz; the one-card measurement kernel is being routed.
Where you come in. See What is not done.
§How uBFT talks to it
| Port | Direction | Beat | Content |
|---|---|---|---|
s_axis_sign | uBFT → service | 512 bits | bytes 0..16 message (uBFT’s {view, slot, k, m}), 16..24 tag |
m_axis_frame | service → uBFT | 26 × 512 bits | the 1,642-byte signature, tag at 1642..1650 |
s_axis_verify | uBFT → service | 27 × 512 bits | header (key slot, tag), then the frame with the message at 1642..1658 |
m_axis_verdict | service → uBFT | 64 bits | verdict (0x80 valid), slot, tag, in request order |
On uBFT’s side (branch dsig-integration of a local ubft_new clone, not
pushed) a compile switch UBFT_SIG_SCHEME picks the original inline
Ed25519 (0), the DSig service (1) or this repository’s Ed25519 service (2);
config.cfg connects the four streams, the Makefile links our exported
.xo at 205 MHz beside uBFT’s 300 MHz kernels, and each share in the HBM
mailbox grows from 64 to 1,664 bytes. The branch also fixes two bugs that
kept uBFT’s slow path from reaching a remote quorum (the valid flag was
never sent to peers; every poll re-verified every share).
§The optimal configuration for uBFT
A slot needs one signature and 2f verifications; even one slice signs
6 million per second. So the right build is the smallest, and latency, not
rate, is the measure:
| Choice | Value | Why |
|---|---|---|
| Message profile | 64 B (profile 2, DSIG_MESSAGE_BYTES=64) | signs uBFT’s whole 48-byte context (session, role, CTB fields), so U33 is closed and CTB can be signed; profile 1 (16 B: view, slot, k, m) builds only with the Tuner’s profile 1 for uBFT set to allowed |
| Signer slices | 2 | the smallest buildable signer; a lone request uses one idle slice, so more slices add LUTs, not speed |
| T-table rounds | 4 (DSIG_TABLE_MASK=0x78) | the measured builds’ setting; fewest LUTs that meet 205 MHz |
| Verify units | 1 | 2f verifications can pipeline through one unit at ~36 cycles each after the first (uBFT’s branch still sends one at a time, SIG_VERIFY_MAX_INFLIGHT=1, so today it pays 3.6 µs each) |
| Ed25519 verify core | 1,1,1,0, fix14 | a certificate check per peer per 128 signatures |
| Key slots | capacity 8, at most 7 active (uBFT needs an odd membership); 16 with the Ed25519 crate’s key-slots-16 | indexed by replica, own slot included |
| Clock | 205 MHz, its own clock domain | uBFT’s kernels keep 300 MHz; v++ inserts the stream clock crossing |
cargo tuner with --features ubft plans exactly this beside the uBFT
replica (paper “uBFT … DSig”) and shows each slot’s crypto time against the
inline baseline.
§The ≥5× target and how it is measured
The user’s target (2026-10-21) is at least 5× uBFT’s performance, measured on the card: completed, correct operations per second at the same replica count, workload, identity security, payload and cache state (warm or cold), with the clock measured on the card, finite bursts and continuous runs reported separately. It is a target, not a result.
Where the 5× has to come from:
| Workload | Upstream’s bottleneck | What removes it |
|---|---|---|
| fast path (normal operation; no crypto) | a 161.3 MHz clock (the inline signer is the worst path), one QP per FSM step, mostly one slot at a time | crypto out of the consensus kernel (37,971 LUT at 3.12 ns est. instead of 234,341 at 4.68 ns), so the controller can run at 300 MHz; all-peer broadcast; several slots in flight (rhdl_ubft) |
| slow path (any timeout or fault) | 19.0 ms per signature, 64.5 ms per verification, one after another | this service: 1.01–1.06 µs sign (whole kernel), 3.6 µs verify cached, the announce removes the certificate miss, verifications pipelined |
The campaign (one table per scheme):
| Row | What runs | Expected |
|---|---|---|
| pinned baseline | ubft_new a7537bd, unmodified | no slow-path progress: reported as failure / zero, not hidden |
| repaired baseline | patches 0001 (valid-flag broadcast) and the QP-table decode only, inline Ed25519 | the reference for every ratio |
| rhdl_ubft + Ed25519 | this repository’s Ed25519 service | |
| rhdl_ubft + DSig | this service, announce on |
Each row is run at N = 3 with warm and cold caches, on a fast-path-only workload and a forced-slow-path one, and prints operations/s, slot latency and the ratio to the repaired baseline with pass/fail against 5×.
§What is not done
- No card number for the service yet.
UbftCard(the service and a probe that plays uBFT on one card) is being routed; until it runs, the latencies above are native RHDL simulation. - Not linked with uBFT. uBFT’s other kernels (RoCE stack, kvstore,
hash table) come from its own IP repository; the combined
xclbinhas not been built, and a real slot needs two or more cards on a network. - A certificate miss costs ~52,670 cycles (257 µs) of Ed25519. The certificate announce (built) checks each peer batch’s certificate ahead of use; a share that arrives before its announce still pays it.
- At most 7 active replicas with 8 key slots (uBFT allows 12).
- Bigger messages on the wire. A DSig share is 1,664 bytes (1,728 in profile 2) against Ed25519’s 64; uBFT’s RDMA writes go out as 26 (27) writes of 64 bytes (the larger RoCE write mode is untested).
- The uBFT apply contract (
crates/planner/src/build_apply.rs) still pinsDSIG_MESSAGE_BYTES=16; it needs the Tuner’s message profile and gate before a profile-2 replica can be applied from a saved plan. DSig as the signature service of a uBFT replica (featureubft).
§The problem
uBFT is a consensus engine that
runs entirely on an Alveo U280: a Vitis HLS state machine (ubft) that
talks to its peers through a RoCE stack and keeps its mailboxes in HBM.
On its slow path every replica signs a 64-bit summary of the slot, sends
the signature to every peer, and verifies the shares it receives until
it holds 2f + 1. It does both with the Vitis Security Library’s
Ed25519 (xf::security::eddsaEd25519), called inline: the replica’s
whole state machine stops for every signature and every verification,
and that signer is the routed design’s worst path (−2.866 ns), which
pulls the shared kernel clock from 300 MHz down to 161.3 MHz.
The rest of this crate signs and verifies in batches: the host fills HBM, starts a kernel, and reads frames or verdicts back when it is done. A consensus engine cannot work that way. It needs a service that sits next to it on the chip, takes one request at a time on a stream, and answers in microseconds.
§The idea
[UbftEngine] is that service: the same slices, certificate unit, verify
core and Ed25519 verify core as the batch kernels, with AXI4-Stream ports
where those have HBM masters.
uBFT (HLS) ──sign request──▶ ┌──────────────── UbftEngine ───────────────┐
◀──26-beat frame── │ UbftSigner: dealer ▶ slices ▶ arbiter │
│ cert unit (Ed25519 signer) │
uBFT (HLS) ──verify request─▶ │ UbftVerifier: framing ▶ verify core ▶ tags│
◀──verdict word─── │ Ed25519 verify core, 8 trusted key slots │
└──────── AXI4-Lite: keys, counters ────────┘- Signing. A request is one 64-byte beat: the 16-byte message and an
8-byte tag. The dealer hands it to the first idle slice; the slice
signs it with a one-time key it made in advance, and the frame (26
beats, the format of
crate::slice_signer, the tag in the message index field) leaves on the frame stream. Keys are made all the time in the background, so a request never waits for key generation. - Verifying. A request is a header beat (the signer’s key slot and a 32-bit tag) followed by the 26 beats of the frame exactly as the signer produced it. The verdict word comes back in request order.
- Peers. uBFT has one signer per replica, so the verifier trusts up to
[
KEY_SLOTS] Ed25519 keys at once (replicasN ≤ 9). The certificate cache keeps the key slot with every entry (seecrate::verify::certs): a certificate checked for one replica never answers for another.
§Streams
| Port | Direction | Beat | Content |
|---|---|---|---|
s_axis_sign | in | 512 bits | bytes 0..16 message, 16..24 tag (LE64) |
m_axis_frame | out | 512 bits, tlast on beat 25 | the frame: 1,642-byte signature, tag at 1642..1650 |
s_axis_verify | in | 512 bits, tlast on beat 26 | beat 0: byte 0 key slot (4 bits), bytes 4..8 tag (LE32); beats 1..26: the frame |
m_axis_verdict | out | 64 bits | bits 0..8 verdict (crate::verify), 8..12 key slot, 32..64 tag |
m_axis_cert | out | 512 bits, tlast on beat 1 | this signer’s new batch certificate: commitment, then Ed25519 signature |
s_axis_cert | in | 512 bits, tlast on beat 2 | a peer’s announced certificate: header (byte 0 key slot), commitment, signature |
The certificate streams are optional: uBFT forwards m_axis_cert to
its peers and feeds theirs into s_axis_cert, so a batch’s certificate
is checked before its first signature arrives (a cache miss otherwise
costs one Ed25519 check, ~52,670 cycles). Leave s_axis_cert idle and
m_axis_cert drained to skip it.
A verdict of 0x80 means valid; any other value has bit 7 set and names
the failed checks.
§Register map (32-bit words)
| Offset | Name | Access |
|---|---|---|
0x000 | AP_CTRL: bit 0 ap_start, 1 ap_done (clear on read), 2 ap_idle, 3 ap_ready | RW |
0x004/0x008/0x00c | GIE / IER / ISR | RW |
0x010 | arg sign_count: signatures this session (u32) | W |
0x014 | arg verify_count: verifications this session (u32) | W |
0x018 | arg batches: key batches to make (u32, 128 keys each) | W |
0x020 | arg epoch (u64) | W |
0x028 | arg first_batch (u64) | W |
0x030 + 4w | args cv0..cv7: derive-key prefix CV (secret) | W |
0x050 + 4w | args seed0..seed7: Ed25519 seed (secret) | W |
0x070 | arg key_mask: trusted key slots in use (bits 0..8) | W |
0x080 + 0x20·s + 4w | args key{s}_{w}: slot s’s Ed25519 public key, bytes 4w..4w+4 LE | W |
0x180 + 8·s | args epoch{s} (u64, lo then hi): slot s’s signer’s epoch this session | W |
0x1c0 | arg epoch_mask: slots whose epoch is enforced (bits 0..8; see [verifier], session epochs) | W |
0x200 | MAGIC = [MAGIC] (“DSU1”) | R |
0x204 | ABI = [ABI_VERSION] | R |
0x208 | SHAPE: slices bits 0..8, table mask 8..24, key slots 24..28 | R |
0x20c | FAULT: bit 0 slice, 1 certificate unit, 2 Ed25519 verify core, 3 a trusted key rejected, 4 verify stream framing | R |
0x210/0x214 | TICKS lo (snapshots hi) / hi | R |
0x218/0x21c | START_TICK lo/hi | R |
0x220/0x224 | DONE_TICK lo/hi | R |
0x228/0x22c | KEY_TICK lo/hi: the trusted keys finished loading | R |
0x230 | SIGNED: frames sent | R |
0x234 | SIGN_TAKEN: sign requests accepted | R |
0x238 | VERIFIED: verdicts sent | R |
0x23c | VALID: verdicts that were 0x80 | R |
0x240 | CERT_CHECKS: Ed25519 verifications issued | R |
0x244 | CERT_FAILS | R |
0x248 | CERTIFIED: batches the signer certified | R |
0x24c | ANNOUNCED: certificates sent on m_axis_cert | R |
0x250 + 4i | sign latency: N, LAST, MIN, MAX, SUM lo, SUM hi | R |
0x270 + 4i | verify latency: same six words | R |
0x2a0 + 4w | PUBLIC_KEY: the signer’s Ed25519 public key (8 × u32) | R |
§How latency is measured
The kernel times requests on its own clock. A request that arrives while
no other request of its kind is in flight starts a measurement: the tick
of its first beat’s handshake. The measurement ends on the handshake of
its answer’s last beat, if no second request arrived meanwhile. So N
counts isolated requests, and LAST/MIN/MAX/SUM are their
latencies in cycles, request beat to answer beat, with nothing else in
the service. Loaded latency is not measured here.
§The rules from the batch kernels still hold
One session per reset: the kernel never restarts, the host gives every session a fresh namespace (epoch, batch range, master seed), and the secrets have no read path. A one-time key is burned before its frame leaves, so a stalled frame stream can never cause a key to sign twice.
Re-exports§
pub use card::CardInput;pub use card::CardOutput;pub use card::UbftCard;pub use engine::CfgWrite;pub use engine::UbftEngine;pub use engine::UbftInput;pub use engine::UbftOutput;pub use probe::UbftProbe;pub use signer::UbftSigner;pub use verifier::UbftVerifier;
Modules§
- card
UbftCard: the uBFT service with its probe, as one card kernel for measurement.- engine
UbftEngine: the register file, the trusted-key loader and the Ed25519 verify core aroundUbftSignerandUbftVerifier.- probe
- A uBFT stand-in for measuring
super::UbftEngineon one card. - signer
- The signing half of
super::UbftEngine: a stream in front of the batch engine’s slices and certificate unit. - verifier
- The verifying half of
super::UbftEngine: one verify core behind a request stream.
Structs§
- Axis64
- One 64-bit stream beat (the verdict stream).
- Axis512
- One 512-bit stream beat (
tvalid,tdata,tlast;treadytravels the other way as a plainbool). - Latency
- Isolated-request latency statistics, in cycles.
Constants§
- ABI_
VERSION - Register-map version.
- ARG_
BATCHES batches.- ARG_CV
cv0.- ARG_
EPOCH epoch(u64).- ARG_
EPOCHS epoch{s}(slot 0’s low word; high word at +4, slots at +8).- ARG_
EPOCH_ MASK epoch_mask.- ARG_
FIRST_ BATCH first_batch(u64).- ARG_
KEYS key0_0; slots, wordwat+ 0x20·s + 4w.- ARG_
KEY_ MASK key_mask.- ARG_
SEED seed0.- ARG_
SIGN_ COUNT - Offsets of the host arguments.
- ARG_
VERIFY_ COUNT verify_count.- KEY_
SLOTS - Trusted Ed25519 keys the verifier holds at once (the verify core’s key
slots): replicas
N ≤ KEY_SLOTS + 1. - MAGIC
MAGICregister value (“DSU1” in little-endian ASCII).- REG_ABI
ABI.- REG_
ANNOUNCED ANNOUNCED.- REG_
CERTIFIED CERTIFIED.- REG_
CERT_ CHECKS CERT_CHECKS.- REG_
CERT_ FAILS CERT_FAILS.- REG_
DONE_ TICK DONE_TICKlo.- REG_
FAULT FAULT.- REG_
KEY_ TICK KEY_TICKlo.- REG_
MAGIC - Status registers.
- REG_
PUBLIC_ KEY PUBLIC_KEYwords.- REG_
SHAPE SHAPE.- REG_
SIGNED SIGNED.- REG_
SIGN_ LATENCY - Sign latency block (six words).
- REG_
SIGN_ TAKEN SIGN_TAKEN.- REG_
START_ TICK START_TICKlo.- REG_
TICKS TICKSlo.- REG_
VALID VALID.- REG_
VERIFIED VERIFIED.- REG_
VERIFY_ LATENCY - Verify latency block (six words).
- VERIFY_
IN_ FLIGHT - Verify requests that may be in flight (the verify core’s contexts).
- VERIFY_
REQUEST_ BEATS - Beats of a verify request: the header and the 26-beat frame.
Functions§
- latency_
kernel - Opens or closes a latency measurement.