The Module 6 capstone: a Submitter that signs commands with Ed25519, dedups by command_id, retries transient failures with backoff + ctx cancellation, and stays correct under concurrent use. The exact shape you'll wrap around real Canton gRPC calls.
Where
cd module-06-canton-patterns/exercises/exercise-01-signed-submitter
go test -v -race ./...
What gets tested
Happy path — submit succeeds, signature verifies on the backend side.
Empty ID — returns wrapped ErrPermanent.
Idempotency — same command_id twice → one backend call, same Receipt returned both times.
Transient retry — backend fails twice then succeeds; submitter makes exactly 3 attempts.
No retry on permanent — ErrPermanent → exactly 1 backend call.
Ctx cancellation — long backoff + short ctx timeout → returns quickly with ctx error.
Concurrent safety — 50 goroutines, 5 unique IDs → exactly 5 backend calls (dedup honored even under concurrency).
What this combines
M3: mutexes for the dedup map; ctx-aware select for retry waits.
M4: nothing direct, but the same I/O patterns.
M6 L1: Ed25519 signing.
M6 L3: dedup keys + exponential backoff with jitter.
Once you can write this, the capstone is mostly wiring it to real gRPC.
One non-obvious thing
The dedup check should briefly hold a mutex, find no entry, release the mutex, then do the I/O, then re-lock to write the result. Never hold a mutex across the I/O call — if Submit blocks for a second, every other caller is blocked too, and contention destroys throughput.
This is one of the most common Go bugs in production: the cache lookup and the cache populate happen under the same lock. Don't.