What changed, and why it matters
This commit adds new factory-testing commands for the Tropic secure chip inside Trezor devices. It does not change normal wallet behavior or user funds handling. The new commands are only reachable in a special manufacturing test mode (prodtest), not in the regular firmware that end users run. The code is defensive: it checks command arguments, limits slot ranges, avoids overwriting certificate slots, re-initializes monotonic counters so they are not left depleted, and erases temporary test data. There is no indication this commit fixes a security bug or introduces a meaningful attack path.
No urgent action required. Treat as a normal feature commit for the manufacturing test image. If reviewing further, verify that prodtest commands are not compiled into production firmware builds and that the deterministic PRNG seeding cannot be influenced by untrusted input to change slot selection in a harmful way.
Security signals we found
New prodtest-only CLI commands for Tropic secure-element validation
Argument validation and range clamping for iterations, slot counts, and explicit slots
Deterministic PRNG used only for slot selection, not for cryptographic material
Slot access restricted by session privilege level (privileged vs unprivileged)
Certificate slots 0-5 explicitly excluded from R-memory write tests
Monotonic counters re-initialized to max after test to avoid depleted state
Temporary ECC key erased after signing test
R-memory slots erased after write/read test
No changes to wallet firmware, bootloader, or cryptographic core
Evidence from the diff
The patch expands core/embed/projects/prodtest/cmd/prodtest_tropic.c with focused Tropic secure-element stress and integrity tests (tropic-stress-*, tropic-test-*), deprecates the old combined tropic-stress-test, and adds matching error codes. It replaces find_pairing_key() with tropic_ensure_session(), which now tries privileged, factory, then unprivileged pairing keys. Slot selection is deterministic via a seeded xorshift32 PRNG and is range-checked; explicit-slot mode requires slot_count==1. Tests avoid privileged-only slots when only an unprivileged session is available, skip certificate slots 0-5 in R-memory tests, and restore monotonic counters to max after testing. No user-facing firmware code is modified.
Changed components
core/embed/projects/prodtest/cmd/prodtest_tropic.ccore/embed/projects/prodtest/error_codes.jsoncore/embed/projects/prodtest/prodtest_error_codes.hcore/embed/projects/prodtest/README.mdInspect captured patch +935 / −23
diff --git a/core/embed/projects/prodtest/.changelog.d/7161.added b/core/embed/projects/prodtest/.changelog.d/7161.added
new file mode 100644
index 00000000..f225fa20
--- /dev/null
+++ b/core/embed/projects/prodtest/.changelog.d/7161.added
@@ -0,0 +1 @@
+Add focused `tropic-stress-*` commands and `tropic-test-*` commands that also check operation results.
diff --git a/core/embed/projects/prodtest/.changelog.d/7161.deprecated b/core/embed/projects/prodtest/.changelog.d/7161.deprecated
new file mode 100644
index 00000000..9687b5b8
--- /dev/null
+++ b/core/embed/projects/prodtest/.changelog.d/7161.deprecated
@@ -0,0 +1 @@
+Deprecate `tropic-stress-test`. Use `tropic-stress-*` commands instead.
diff --git a/core/embed/projects/prodtest/README.md b/core/embed/projects/prodtest/README.md
index 957d2968..9cdc9f66 100644
--- a/core/embed/projects/prodtest/README.md
+++ b/core/embed/projects/prodtest/README.md
@@ -1477,9 +1477,33 @@ secure-channel-handshake-2 e08e84b91413ad8f7b07853c8ce4c1b5547a12d9dd65f30e3adaa
OK
```
-### tropic-stress-test
+### tropic-stress-* and tropic-test-*
-Runs a Tropic stress test that repeatedly calls `lt_init()`, `lt_session_start()`, `lt_mac_and_destroy()`, `lt_ecc_key_generate()` and `lt_random_value_get()` to test that Tropic doesn't enter alarm mode.
+The `tropic-stress-*` commands hammer an operation without checking its results, to test that Tropic doesn't enter alarm mode. The `tropic-test-*` commands verify that an operation produces correct results.
+
+The commands that test multiple slots select them pseudorandomly deterministically (seeded with the command and the requested slot count), so a given invocation always tests the same slots. Such a command also accepts an optional trailing `<slot>` argument that pins the exact slot to use; it is only valid together with a slot count of 1. `<iterations>` is the number of test iterations performed *per slot*.
+
+| Command | Arguments | Description |
+|---|---|---|
+| `tropic-stress-init` | `[<iterations> [<delay-ms>]]` | Repeatedly reinitialize the chip, waiting `<delay-ms>` between deinit and init. |
+| `tropic-stress-session` | `[<iterations>]` | Repeatedly tear down and re-establish the secure session. |
+| `tropic-stress-mac-and-destroy` | `[<iterations> <slot-count> [<slot>]]` | Hammer MAC-and-destroy on a sample of slots with random inputs. |
+| `tropic-test-mac-and-destroy` | `[<iterations> <slot-count> [<slot>]]` | Verify MAC-and-destroy produces consistent results; one consistency check per iteration against the first result. |
+| `tropic-test-sign` | `[<iterations>]` | Generate EdDSA signatures with verification; one sign & verify per iteration. |
+| `tropic-test-counter` | `[<iterations> <slot-count> [<slot>]]` | Verify monotonic counters set, read back, and decrement correctly; one decrement & check per iteration. |
+| `tropic-test-rmem` | `[<iterations> <slot-count> [<slot>]]` | Write random data to a sample of R-memory slots and read it back; one write & read cycle per iteration. |
+| `tropic-test-rng` | `[<iterations>]` | Sanity-check the TRNG output (non-zero, non-repeating). |
+| `tropic-stress-test` | `[<init-iterations> <start-session-iterations> <mac-and-destroy-slot-count> <mac-and-destroy-per-slot-iterations> <signing-iterations> <rng-iterations>]` | **Deprecated** — the original combined stress test; will be removed. |
+
+Several resources are partitioned by pairing-key privilege, so the available slot range depends on the session. A privileged session (or, on an unprovisioned device, the factory key) grants the full range; an unprivileged session is restricted:
+
+| Command | Full range | Unprivileged range |
+|-----------------|------------|--------------------|
+| MAC-and-destroy | `0`–`127` | `64`–`127` |
+| counter | `0`–`15` | `4`–`15` |
+| R-memory | `6`–`511` | `256`–`511` |
+
+The R-memory test never touches the certificate slots (`0`–`5`).
### tropic-benchmark
diff --git a/core/embed/projects/prodtest/cmd/prodtest_tropic.c b/core/embed/projects/prodtest/cmd/prodtest_tropic.c
index d5a1d22f..5acbb883 100644
--- a/core/embed/projects/prodtest/cmd/prodtest_tropic.c
+++ b/core/embed/projects/prodtest/cmd/prodtest_tropic.c
@@ -1654,28 +1654,748 @@ cleanup:
tropic_deinit();
}
-static bool find_pairing_key(cli_t* cli, lt_pkey_index_t* pairing_key_index) {
- *pairing_key_index = -1;
-
- for (lt_pkey_index_t i = TROPIC_FACTORY_PAIRING_KEY_SLOT;
- i <= TROPIC_PRIVILEGED_PAIRING_KEY_SLOT; i++) {
- lt_ret_t res = tropic_custom_session_start(cli, i);
- if (res == LT_OK) {
- *pairing_key_index = i;
- return true;
+// Total number of MAC-and-destroy slots.
+#define TROPIC_MAC_AND_DESTROY_SLOT_TOTAL \
+ (2 * TROPIC_MAC_AND_DESTROY_SLOT_COUNT)
+
+// Number of monotonic counters.
+#define TROPIC_MCOUNTER_COUNT (TR01_MCOUNTER_INDEX_15 + 1)
+// For unprivileged sessions, counter initialization is restricted to counters
+// 4-15. Mirrors `CFG_UAP_MCOUNTER_INIT`.
+#define TROPIC_FIRST_UNPRIVILEGED_MCOUNTER 4
+
+// R-memory range used by the writable-slot test. Starts right after the
+// certificate slots (0-5), which must never be overwritten.
+#define TROPIC_RMEM_TEST_FIRST \
+ (TROPIC_DEVICE_CERT_FIRST_SLOT + TROPIC_DEVICE_CERT_SLOT_COUNT)
+#define TROPIC_RMEM_TEST_LAST TR01_R_MEM_DATA_SLOT_MAX
+#define TROPIC_RMEM_TEST_COUNT \
+ (TROPIC_RMEM_TEST_LAST - TROPIC_RMEM_TEST_FIRST + 1)
+// For unprivileged sessions, R-memory writes are restricted to slots 256-511.
+// Mirrors `CFG_UAP_R_MEM_DATA_WRITE`,
+#define TROPIC_RMEM_UNPRIVILEGED_FIRST 256
+// Amount of data written and read back per R-memory slot in the test.
+#define TROPIC_RMEM_TEST_DATA_SIZE 64
+
+// Per-command identifiers used as PRNG seeds so that different commands sample
+// different slot subsets.
+typedef enum {
+ TROPIC_CMD_STRESS_MAC_AND_DESTROY = 1,
+ TROPIC_CMD_TEST_MAC_AND_DESTROY,
+ TROPIC_CMD_TEST_COUNTER,
+ TROPIC_CMD_TEST_RMEM,
+} tropic_command_id_t;
+
+// Deterministic PRNG (xorshift32) used for slot selection only.
+static uint32_t tropic_prng_next(uint32_t* state) {
+ uint32_t x = *state;
+ x ^= x << 13;
+ x ^= x >> 17;
+ x ^= x << 5;
+ *state = x;
+ return x;
+}
+
+// Selects slots for a slot-based test, writing them to `out`. [lo, hi) is the
+// valid range of slots for the test. `*slot_count` is the requested number of
+// slots; it is clamped in place to the number available (`hi - lo`). Returns
+// false (after reporting via `cli`) only for an invalid explicit-slot request.
+static bool tropic_select_slots(cli_t* cli, tropic_command_id_t command_id,
+ uint16_t lo, uint16_t hi, uint32_t* slot_count,
+ int32_t explicit_slot, uint16_t* out) {
+ if (explicit_slot >= 0) {
+ if (*slot_count != 1) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_COUNT,
+ "An explicit slot requires a slot count of 1.");
+ return false;
}
- if (res != LT_L2_HSK_ERR) {
- cli_trace(
- cli,
- "`tropic_custom_session_start()` for key %d failed with error '%s'",
- i, lt_ret_verbose(res));
+ if (explicit_slot < lo || explicit_slot >= hi) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_RANGE,
+ "Slot %d is outside the valid range [%d, %d).", explicit_slot,
+ lo, hi);
+ return false;
}
+ out[0] = explicit_slot;
+ return true;
}
+ size_t max_slots = hi - lo;
+ if (*slot_count > max_slots) {
+ cli_trace(cli, "Clamping slot count to %u available slots.",
+ (unsigned)max_slots);
+ *slot_count = max_slots;
+ }
+
+ uint16_t candidates[max_slots];
+ for (size_t i = 0; i < max_slots; i++) {
+ candidates[i] = lo + i;
+ }
+
+ uint32_t prng_state = ((uint32_t)command_id << 24) ^ *slot_count;
+ for (size_t i = 0; i < *slot_count; i++) {
+ // A Fisher-Yates shuffle step to select a random slot from the remaining
+ // candidates.
+ size_t j = i + (tropic_prng_next(&prng_state) % (max_slots - i));
+ out[i] = candidates[j];
+ candidates[j] = candidates[i];
+ }
+ return true;
+}
+
+// Ensures a secure session is established and reports which pairing key was
+// used if `pairing_key_index` is non-NULL.
+static bool tropic_ensure_session(cli_t* cli,
+ lt_pkey_index_t* pairing_key_index) {
+ static const lt_pkey_index_t keys[] = {
+ TROPIC_PRIVILEGED_PAIRING_KEY_SLOT,
+ TROPIC_FACTORY_PAIRING_KEY_SLOT,
+ TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT,
+ };
+ lt_ret_t results[ARRAY_LENGTH(keys)];
+ for (size_t i = 0; i < ARRAY_LENGTH(keys); i++) {
+ results[i] = tropic_custom_session_start(NULL, keys[i]);
+ if (results[i] == LT_OK) {
+ if (pairing_key_index != NULL) {
+ *pairing_key_index = keys[i];
+ }
+ return true;
+ }
+ }
+
+ // No key worked. Explain why each attempt failed.
+ for (size_t i = 0; i < ARRAY_LENGTH(keys); i++) {
+ cli_trace(
+ cli,
+ "`tropic_custom_session_start()` for key %d failed with error '%s'",
+ keys[i], lt_ret_verbose(results[i]));
+ }
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_NO_PAIRING_KEY,
+ "No pairing key is available");
return false;
}
+static bool tropic_parse_iterations(cli_t* cli, uint32_t* iterations);
+static bool tropic_parse_iterations_and_slots(cli_t* cli, uint32_t* iterations,
+ uint32_t* slot_count,
+ int32_t* explicit_slot);
+
+// Stress test: reinitialize the chip repeatedly to provoke startup faults.
+static void prodtest_tropic_stress_init(cli_t* cli) {
+ uint32_t iterations = 100;
+ uint32_t delay_ms = 0;
+ uint32_t argc = cli_arg_count(cli);
+ if (argc > 2) {
+ cli_error_arg_count(cli);
+ return;
+ }
+ if (argc >= 1 && !cli_arg_uint32(cli, "iterations", &iterations)) {
+ cli_error_arg(cli, "Expecting number of iterations.");
+ return;
+ }
+ if (argc >= 2 && !cli_arg_uint32(cli, "delay-ms", &delay_ms)) {
+ cli_error_arg(cli, "Expecting init delay in ms.");
+ return;
+ }
+ if (iterations == 0) {
+ cli_error_arg(cli, "Iterations must be greater than 0.");
+ return;
+ }
+ cli_trace(cli, "Initialization iterations: %u. Init delay: %u ms.",
+ (unsigned)iterations, (unsigned)delay_ms);
+
+ g_tropic_handshake_state = TROPIC_HANDSHAKE_STATE_0;
+
+ for (int i = 0; i < iterations; i++) {
+ tropic_deinit();
+ // Simulate a delay between suspend and wake-up.
+ systick_delay_ms(delay_ms);
+ if (!tropic_init()) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_INIT,
+ "Call #%d of `tropic_init()` failed", i + 1);
+ return;
+ }
+ if (!tropic_wait_for_ready(cli)) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_READY,
+ "Call #%d of `tropic_wait_for_ready()` failed", i + 1);
+ return;
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Stress test: tear down and re-establish the secure session repeatedly.
+static void prodtest_tropic_stress_session(cli_t* cli) {
+ uint32_t iterations = 5;
+ if (!tropic_parse_iterations(cli, &iterations)) {
+ return;
+ }
+ cli_trace(cli, "Session iterations: %u.", (unsigned)iterations);
+
+ lt_pkey_index_t pairing_key_index = -1;
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
+ return;
+ }
+
+ for (int i = 0; i < iterations; i++) {
+ lt_ret_t res = tropic_session_invalidate();
+ if (res != LT_OK) {
+ cli_error(
+ cli, PRODTEST_ERR_TROPIC_STRESS_SESSION_INVALIDATE,
+ "Call #%d of `tropic_session_invalidate()` failed with error '%s'",
+ i + 1, lt_ret_verbose(res));
+ return;
+ }
+ res = tropic_custom_session_start(cli, pairing_key_index);
+ if (res != LT_OK) {
+ cli_error(
+ cli, PRODTEST_ERR_TROPIC_STRESS_SESSION_START,
+ "Call #%d of `tropic_custom_session_start()` failed with error '%s'",
+ i + 1, lt_ret_verbose(res));
+ return;
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Stress test: hammer MAC-and-destroy on a sample of slots with random inputs,
+// without checking the results, to provoke alarm mode.
+static void prodtest_tropic_stress_mac_and_destroy(cli_t* cli) {
+ uint32_t iterations = 3;
+ uint32_t slot_count = TROPIC_MAC_AND_DESTROY_SLOT_TOTAL;
+ int32_t explicit_slot = -1;
+ if (!tropic_parse_iterations_and_slots(cli, &iterations, &slot_count,
+ &explicit_slot)) {
+ return;
+ }
+ cli_trace(cli, "Iterations per slot: %u. Slot count: %u. Explicit slot: %d.",
+ (unsigned)iterations, (unsigned)slot_count, (int)explicit_slot);
+
+ lt_pkey_index_t pairing_key_index = -1;
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
+ return;
+ }
+ bool unprivileged = pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT;
+ if (unprivileged) {
+ cli_trace(cli,
+ "Privileged session unavailable; sampling only the unprivileged "
+ "MAC-and-destroy range.");
+ }
+ uint16_t first = unprivileged ? TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED
+ : TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED;
+
+ uint16_t slots[TROPIC_MAC_AND_DESTROY_SLOT_TOTAL];
+ if (!tropic_select_slots(cli, TROPIC_CMD_STRESS_MAC_AND_DESTROY, first,
+ TROPIC_MAC_AND_DESTROY_SLOT_TOTAL, &slot_count,
+ explicit_slot, slots)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ for (int s = 0; s < slot_count; s++) {
+ lt_mac_and_destroy_slot_t slot = slots[s];
+ for (int i = 0; i < iterations; i++) {
+ uint8_t buffer[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ rng_fill_buffer(buffer, sizeof(buffer));
+ lt_ret_t res = lt_mac_and_destroy(h, slot, buffer, buffer);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_MAC_AND_DESTROY,
+ "Call #%d of `lt_mac_and_destroy()` for slot %d failed with "
+ "error '%s'",
+ i + 1, slot, lt_ret_verbose(res));
+ return;
+ }
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Integrity test: verify MAC-and-destroy produces consistent results on a
+// sample of slots.
+// for each slot in sample_slots:
+// generate reset_key and input randomly
+// M&D(reset_key)
+// output_0 = M&D(input)
+// for i in 1..iterations:
+// M&D(reset_key)
+// output = M&D(input)
+// assert output == output_0
+static void prodtest_tropic_test_mac_and_destroy(cli_t* cli) {
+ uint32_t iterations = 2;
+ uint32_t slot_count = TROPIC_MAC_AND_DESTROY_SLOT_TOTAL / 2;
+ int32_t explicit_slot = -1;
+ if (!tropic_parse_iterations_and_slots(cli, &iterations, &slot_count,
+ &explicit_slot)) {
+ return;
+ }
+ cli_trace(cli, "Iterations per slot: %u. Slot count: %u. Explicit slot: %d.",
+ (unsigned)iterations, (unsigned)slot_count, (int)explicit_slot);
+
+ lt_pkey_index_t pairing_key_index = -1;
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
+ return;
+ }
+ bool unprivileged = pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT;
+ if (unprivileged) {
+ cli_trace(cli,
+ "Privileged session unavailable; sampling only the unprivileged "
+ "MAC-and-destroy range.");
+ }
+ uint16_t first = unprivileged ? TROPIC_FIRST_MAC_AND_DESTROY_SLOT_UNPRIVILEGED
+ : TROPIC_FIRST_MAC_AND_DESTROY_SLOT_PRIVILEGED;
+
+ uint16_t slots[TROPIC_MAC_AND_DESTROY_SLOT_TOTAL];
+ if (!tropic_select_slots(cli, TROPIC_CMD_TEST_MAC_AND_DESTROY, first,
+ TROPIC_MAC_AND_DESTROY_SLOT_TOTAL, &slot_count,
+ explicit_slot, slots)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ for (int s = 0; s < slot_count; s++) {
+ lt_mac_and_destroy_slot_t slot = slots[s];
+
+ uint8_t reset_key[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ uint8_t input[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ uint8_t output_0[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ uint8_t output[TROPIC_MAC_AND_DESTROY_SIZE] = {0};
+ rng_fill_buffer(reset_key, sizeof(reset_key));
+ rng_fill_buffer(input, sizeof(input));
+
+ // Setup: M&D(reset_key)
+ lt_ret_t res = lt_mac_and_destroy(h, slot, reset_key, output);
+ if (res != LT_OK) {
+ cli_error(
+ cli, PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY,
+ "`lt_mac_and_destroy()` setup for slot %d failed with error '%s'",
+ slot, lt_ret_verbose(res));
+ return;
+ }
+ // Measure: output_0 = M&D(input)
+ res = lt_mac_and_destroy(h, slot, input, output_0);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY,
+ "`lt_mac_and_destroy()` initial measurement for slot %d failed "
+ "with error '%s'",
+ slot, lt_ret_verbose(res));
+ return;
+ }
+
+ for (int i = 0; i < iterations; i++) {
+ // Reset: M&D(reset_key)
+ res = lt_mac_and_destroy(h, slot, reset_key, output);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY,
+ "`lt_mac_and_destroy()` reset for slot %d failed at "
+ "iteration #%d with error '%s'",
+ slot, i + 1, lt_ret_verbose(res));
+ return;
+ }
+ // Re-measure: output = M&D(input)
+ res = lt_mac_and_destroy(h, slot, input, output);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY,
+ "`lt_mac_and_destroy()` re-measurement for slot %d failed at "
+ "iteration #%d with error '%s'",
+ slot, i + 1, lt_ret_verbose(res));
+ return;
+ }
+ if (memcmp(output, output_0, sizeof(output_0)) != 0) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY_MISMATCH,
+ "MAC-and-destroy inconsistent on slot %d at iteration #%d",
+ slot, i + 1);
+ return;
+ }
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Integrity test: generate an ECC key, then sign random messages and verify
+// each signature against the slot's public key.
+static void prodtest_tropic_test_sign(cli_t* cli) {
+ uint32_t iterations = 10;
+ if (!tropic_parse_iterations(cli, &iterations)) {
+ return;
+ }
+ cli_trace(cli, "Signing iterations: %u. ECC slot: %d.", (unsigned)iterations,
+ TR01_ECC_SLOT_31);
+
+ if (!tropic_ensure_session(cli, NULL)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ // Slot 31 is usable by both privileged and unprivileged sessions.
+ lt_ecc_slot_t ecc_slot = TR01_ECC_SLOT_31;
+
+ ed25519_public_key public_key = {0};
+ uint8_t message[32] = {0};
+ ed25519_signature signature = {0};
+
+ lt_ret_t res = lt_ecc_key_generate(h, ecc_slot, TR01_CURVE_ED25519);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_KEY_GENERATE,
+ "`lt_ecc_key_generate()` for slot %d failed with error '%s'",
+ ecc_slot, lt_ret_verbose(res));
+ goto cleanup_error;
+ }
+
+ lt_ecc_curve_type_t curve_type = 0;
+ lt_ecc_key_origin_t origin = 0;
+ res = lt_ecc_key_read(h, ecc_slot, public_key, sizeof(public_key),
+ &curve_type, &origin);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_SIGN_KEY_READ,
+ "`lt_ecc_key_read()` for slot %d failed with error '%s'",
+ ecc_slot, lt_ret_verbose(res));
+ goto cleanup_error;
+ }
+ if (curve_type != TR01_CURVE_ED25519) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_SIGN_CURVE,
+ "Curve type on slot %d is not Ed25519 (got %d)", ecc_slot,
+ curve_type);
+ goto cleanup_error;
+ }
+
+ for (int i = 0; i < iterations; i++) {
+ rng_fill_buffer(message, sizeof(message));
+ res = lt_ecc_eddsa_sign(h, ecc_slot, message, sizeof(message), signature);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_EDDSA_SIGN,
+ "Call #%d of `lt_ecc_eddsa_sign()` for slot %d failed with "
+ "error '%s'",
+ i + 1, ecc_slot, lt_ret_verbose(res));
+ goto cleanup_error;
+ }
+ if (ed25519_sign_open(message, sizeof(message), public_key, signature) !=
+ 0) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_SIGN_VERIFY,
+ "Signature #%d for slot %d failed verification", i + 1,
+ ecc_slot);
+ goto cleanup_error;
+ }
+ }
+
+ res = lt_ecc_key_erase(h, ecc_slot);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_KEY_ERASE,
+ "`lt_ecc_key_erase()` for slot %d failed with error '%s'",
+ ecc_slot, lt_ret_verbose(res));
+ return;
+ }
+ cli_ok(cli, "");
+ return;
+
+cleanup_error:
+ lt_ecc_key_erase(h, ecc_slot);
+ return;
+}
+
+// Integrity test: verify monotonic counters set, read back, and decrement
+// correctly on a sample of counters.
+static void prodtest_tropic_test_counter(cli_t* cli) {
+ uint32_t iterations = 5;
+ uint32_t slot_count = TROPIC_MCOUNTER_COUNT;
+ int32_t explicit_slot = -1;
+ if (!tropic_parse_iterations_and_slots(cli, &iterations, &slot_count,
+ &explicit_slot)) {
+ return;
+ }
+ cli_trace(cli,
+ "Iterations per counter: %u. Counters: %u. Explicit counter: %d.",
+ (unsigned)iterations, (unsigned)slot_count, (int)explicit_slot);
+
+ lt_pkey_index_t pairing_key_index = -1;
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
+ return;
+ }
+ bool unprivileged = pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT;
+ if (unprivileged) {
+ cli_trace(cli,
+ "Privileged session unavailable; testing only the unprivileged "
+ "counters.");
+ }
+
+ uint16_t slots[TROPIC_MCOUNTER_COUNT];
+ uint16_t first = unprivileged ? TROPIC_FIRST_UNPRIVILEGED_MCOUNTER : 0;
+ if (!tropic_select_slots(cli, TROPIC_CMD_TEST_COUNTER, first,
+ TROPIC_MCOUNTER_COUNT, &slot_count, explicit_slot,
+ slots)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ for (int s = 0; s < slot_count; s++) {
+ lt_mcounter_index_t idx = slots[s];
+
+ lt_ret_t res = lt_mcounter_init(h, idx, iterations);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT,
+ "`lt_mcounter_init()` for counter %d failed with error '%s'",
+ idx, lt_ret_verbose(res));
+ return;
+ }
+
+ uint32_t value = 0;
+ res = lt_mcounter_get(h, idx, &value);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_GET,
+ "`lt_mcounter_get()` for counter %d failed with error '%s'",
+ idx, lt_ret_verbose(res));
+ return;
+ }
+ if (value != iterations) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT_MISMATCH,
+ "Counter %d read %d after init, expected %d", idx, value,
+ iterations);
+ return;
+ }
+
+ for (int i = 0; i < iterations; i++) {
+ res = lt_mcounter_update(h, idx);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_UPDATE,
+ "`lt_mcounter_update()` for counter %d failed at iteration "
+ "#%d with error '%s'",
+ idx, i + 1, lt_ret_verbose(res));
+ return;
+ }
+ res = lt_mcounter_get(h, idx, &value);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_GET,
+ "`lt_mcounter_get()` for counter %d failed at iteration #%d "
+ "with error '%s'",
+ idx, i + 1, lt_ret_verbose(res));
+ return;
+ }
+ uint32_t expected = iterations - i - 1;
+ if (value != expected) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_MISMATCH,
+ "Counter %d read %d after %d decrements, expected %d", idx,
+ value, i + 1, expected);
+ return;
+ }
+ }
+
+ // Re-initialize counter so it is never left depleted.
+ res = lt_mcounter_init(h, idx, TR01_MCOUNTER_VALUE_MAX);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT,
+ "`lt_mcounter_init()` for counter %d failed with error '%s'",
+ idx, lt_ret_verbose(res));
+ return;
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Integrity test: write random data to a sample of R-memory slots and read it
+// back. Restricted to the range above the certificate slots so it can never
+// overwrite them.
+static void prodtest_tropic_test_rmem(cli_t* cli) {
+ uint32_t iterations = 1;
+ uint32_t slot_count = 25;
+ int32_t explicit_slot = -1;
+ if (!tropic_parse_iterations_and_slots(cli, &iterations, &slot_count,
+ &explicit_slot)) {
+ return;
+ }
+ cli_trace(cli, "Iterations per slot: %u. Slot count: %u. Explicit slot: %d.",
+ (unsigned)iterations, (unsigned)slot_count, (int)explicit_slot);
+
+ lt_pkey_index_t pairing_key_index = -1;
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
+ return;
+ }
+ bool unprivileged = pairing_key_index == TROPIC_UNPRIVILEGED_PAIRING_KEY_SLOT;
+ if (unprivileged) {
+ cli_trace(cli,
+ "Privileged session unavailable; testing only the unprivileged "
+ "R-memory range.");
+ }
+
+ uint16_t slots[TROPIC_RMEM_TEST_COUNT];
+ uint16_t first =
+ unprivileged ? TROPIC_RMEM_UNPRIVILEGED_FIRST : TROPIC_RMEM_TEST_FIRST;
+ if (!tropic_select_slots(cli, TROPIC_CMD_TEST_RMEM, first,
+ TROPIC_RMEM_TEST_LAST + 1, &slot_count,
+ explicit_slot, slots)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ for (int s = 0; s < slot_count; s++) {
+ uint16_t slot = slots[s];
+ for (int i = 0; i < iterations; i++) {
+ uint8_t write_data[TROPIC_RMEM_TEST_DATA_SIZE] = {0};
+ uint8_t read_data[TROPIC_RMEM_TEST_DATA_SIZE] = {0};
+ rng_fill_buffer(write_data, sizeof(write_data));
+
+ // A write to a non-empty slot fails, so erase first (this also clears the
+ // data written by the previous iteration).
+ lt_ret_t res = lt_r_mem_data_erase(h, slot);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RMEM_ERASE,
+ "`lt_r_mem_data_erase()` for slot %d failed at iteration #%d "
+ "with error '%s'",
+ slot, i + 1, lt_ret_verbose(res));
+ return;
+ }
+ res = lt_r_mem_data_write(h, slot, write_data, sizeof(write_data));
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RMEM_WRITE,
+ "`lt_r_mem_data_write()` for slot %d failed at iteration #%d "
+ "with error '%s'",
+ slot, i + 1, lt_ret_verbose(res));
+ lt_r_mem_data_erase(h, slot);
+ return;
+ }
+ uint16_t read_size = 0;
+ res =
+ lt_r_mem_data_read(h, slot, read_data, sizeof(read_data), &read_size);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RMEM_READ,
+ "`lt_r_mem_data_read()` for slot %d failed at iteration #%d "
+ "with error '%s'",
+ slot, i + 1, lt_ret_verbose(res));
+ lt_r_mem_data_erase(h, slot);
+ return;
+ }
+ if (read_size != sizeof(write_data) ||
+ memcmp(read_data, write_data, sizeof(write_data)) != 0) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RMEM_MISMATCH,
+ "R-memory slot %d read-back mismatch at iteration #%d", slot,
+ i + 1);
+ lt_r_mem_data_erase(h, slot);
+ return;
+ }
+ }
+
+ // Leave the slot empty.
+ lt_ret_t res = lt_r_mem_data_erase(h, slot);
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RMEM_ERASE,
+ "`lt_r_mem_data_erase()` for slot %d failed with error '%s'",
+ slot, lt_ret_verbose(res));
+ return;
+ }
+ }
+ cli_ok(cli, "");
+}
+
+// Integrity test: sanity-check the TRNG output (non-zero and not identical to
+// the previous value).
+static void prodtest_tropic_test_rng(cli_t* cli) {
+ uint32_t iterations = 100;
+ if (!tropic_parse_iterations(cli, &iterations)) {
+ return;
+ }
+ cli_trace(cli, "RNG iterations: %u.", (unsigned)iterations);
+
+ if (!tropic_ensure_session(cli, NULL)) {
+ return;
+ }
+
+ lt_handle_t* h = tropic_get_handle();
+ uint8_t previous[32] = {0};
+ for (int i = 0; i < iterations; i++) {
+ uint8_t value[32] = {0};
+ lt_ret_t res = lt_random_value_get(h, value, sizeof(value));
+ if (res != LT_OK) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_RANDOM_GET,
+ "Call #%d of `lt_random_value_get()` failed with error '%s'",
+ i + 1, lt_ret_verbose(res));
+ return;
+ }
+
+ bool all_zero = true;
+ for (size_t j = 0; j < sizeof(value); j++) {
+ if (value[j] != 0) {
+ all_zero = false;
+ break;
+ }
+ }
+ if (all_zero) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RNG_ZERO,
+ "RNG returned an all-zero value at iteration #%d", i + 1);
+ return;
+ }
+ if (i > 0 && memcmp(value, previous, sizeof(value)) == 0) {
+ cli_error(cli, PRODTEST_ERR_TROPIC_TEST_RNG_REPEAT,
+ "RNG returned a repeated value at iteration #%d", i + 1);
+ return;
+ }
+ memcpy(previous, value, sizeof(value));
+ }
+ cli_ok(cli, "");
+}
+
+static bool tropic_parse_iterations(cli_t* cli, uint32_t* iterations) {
+ if (cli_arg_count(cli) > 1) {
+ cli_error_arg_count(cli);
+ return false;
+ }
+ if (cli_arg_count(cli) != 0 &&
+ !cli_arg_uint32(cli, "iterations", iterations)) {
+ cli_error_arg(cli, "Expecting number of iterations.");
+ return false;
+ }
+ if (*iterations == 0) {
+ cli_error_arg(cli, "Iterations must be greater than 0.");
+ return false;
+ }
+ return true;
+}
+
+// Parses `[<iterations> <slot_count> [<slot>]]`. `iterations` is the number of
+// test iterations per slot. The optional third argument pins the exact slot to
+// use (only valid with a slot count of 1).
+static bool tropic_parse_iterations_and_slots(cli_t* cli, uint32_t* iterations,
+ uint32_t* slot_count,
+ int32_t* explicit_slot) {
+ *explicit_slot = -1;
+ uint32_t argc = cli_arg_count(cli);
+ if (argc != 0 && argc != 2 && argc != 3) {
+ cli_error_arg_count(cli);
+ return false;
+ }
+ if (argc >= 2) {
+ if (!cli_arg_uint32(cli, "iterations", iterations)) {
+ cli_error_arg(cli, "Expecting number of iterations.");
+ return false;
+ }
+ if (!cli_arg_uint32(cli, "slot-count", slot_count)) {
+ cli_error_arg(cli, "Expecting slot count.");
+ return false;
+ }
+ }
+ if (*iterations == 0) {
+ cli_error_arg(cli, "Iterations must be greater than 0.");
+ return false;
+ }
+ if (*slot_count == 0) {
+ cli_error_arg(cli, "Slot count must be greater than 0.");
+ return false;
+ }
+ if (argc == 3) {
+ uint32_t slot = 0;
+ if (!cli_arg_uint32(cli, "slot", &slot)) {
+ cli_error_arg(cli, "Expecting slot number.");
+ return false;
+ }
+ *explicit_slot = slot;
+ }
+ return true;
+}
+
+// DEPRECATED: superseded by the `tropic-stress-*` and `tropic-test-*` commands;
+// will be removed in a future release. Retains its original behavior and
+// parameter set for backward compatibility.
static void prodtest_tropic_stress_test(cli_t* cli) {
+ cli_trace(
+ cli,
+ "DEPRECATED: `tropic-stress-test` will be removed; use `tropic-test`.");
+
if (cli_arg_count(cli) > 6) {
cli_error_arg_count(cli);
return;
@@ -1750,9 +2470,7 @@ static void prodtest_tropic_stress_test(cli_t* cli) {
lt_ret_t res = LT_FAIL;
lt_pkey_index_t pairing_key_index = -1;
- if (!find_pairing_key(cli, &pairing_key_index)) {
- cli_error(cli, PRODTEST_ERR_TROPIC_STRESS_NO_PAIRING_KEY,
- "No pairing key is available");
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
return;
}
@@ -1855,9 +2573,7 @@ static void prodtest_tropic_benchmark(cli_t* cli) {
lt_ret_t res = LT_FAIL;
lt_pkey_index_t pairing_key_index = -1;
- if (!find_pairing_key(cli, &pairing_key_index)) {
- cli_error(cli, PRODTEST_ERR_TROPIC_BENCHMARK_NO_PAIRING_KEY,
- "No pairing key is available");
+ if (!tropic_ensure_session(cli, &pairing_key_index)) {
return;
}
@@ -2404,10 +3120,66 @@ PRODTEST_CLI_CMD(
.args = ""
);
+PRODTEST_CLI_CMD(
+ .name = "tropic-stress-init",
+ .func = prodtest_tropic_stress_init,
+ .info = "Stress test Tropic initialization",
+ .args = "[<iterations> [<delay-ms>]]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-stress-session",
+ .func = prodtest_tropic_stress_session,
+ .info = "Stress test Tropic session establishment",
+ .args = "[<iterations>]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-stress-mac-and-destroy",
+ .func = prodtest_tropic_stress_mac_and_destroy,
+ .info = "Stress test Tropic MAC-and-destroy, one MAC-and-destroy per iteration",
+ .args = "[<iterations> <slot-count> [<slot>]]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-test-mac-and-destroy",
+ .func = prodtest_tropic_test_mac_and_destroy,
+ .info = "Check Tropic MAC-and-destroy, one consistency check per iteration against first result",
+ .args = "[<iterations> <slot-count> [<slot>]]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-test-sign",
+ .func = prodtest_tropic_test_sign,
+ .info = "Check Tropic EdDSA signing, one sign & verify per iteration",
+ .args = "[<iterations>]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-test-counter",
+ .func = prodtest_tropic_test_counter,
+ .info = "Check Tropic monotonic counter integrity, one decrement & check per iteration",
+ .args = "[<iterations> <slot-count> [<slot>]]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-test-rmem",
+ .func = prodtest_tropic_test_rmem,
+ .info = "Check Tropic R-memory slot integrity, one write & read cycle per iteration",
+ .args = "[<iterations> <slot-count> [<slot>]]"
+);
+
+PRODTEST_CLI_CMD(
+ .name = "tropic-test-rng",
+ .func = prodtest_tropic_test_rng,
+ .info = "Sanity-check Tropic TRNG output",
+ .args = "[<iterations>]"
+);
+
PRODTEST_CLI_CMD(
.name = "tropic-stress-test",
.func = prodtest_tropic_stress_test,
- .info = "Run stress test for Tropic",
+ .info = "DEPRECATED: use tropic-test; will be removed",
.args = "[<init-iterations> <start-session-iterations> <mac-and-destroy-slot-count> <mac-and-destroy-per-slot-iterations> <signing-iterations> <rng-iterations>]"
);
diff --git a/core/embed/projects/prodtest/error_codes.json b/core/embed/projects/prodtest/error_codes.json
index d2f57432..cb315cb6 100644
--- a/core/embed/projects/prodtest/error_codes.json
+++ b/core/embed/projects/prodtest/error_codes.json
@@ -1536,6 +1536,101 @@
"name": "PRODTEST_ERR_TROPIC_BENCHMARK_RANDOM_GET",
"module": "tropic"
},
+ {
+ "code": 20120,
+ "name": "PRODTEST_ERR_TROPIC_STRESS_MAC_AND_DESTROY",
+ "module": "tropic"
+ },
+ {
+ "code": 20121,
+ "name": "PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY",
+ "module": "tropic"
+ },
+ {
+ "code": 20122,
+ "name": "PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY_MISMATCH",
+ "module": "tropic"
+ },
+ {
+ "code": 20123,
+ "name": "PRODTEST_ERR_TROPIC_TEST_SIGN_KEY_READ",
+ "module": "tropic"
+ },
+ {
+ "code": 20124,
+ "name": "PRODTEST_ERR_TROPIC_TEST_SIGN_CURVE",
+ "module": "tropic"
+ },
+ {
+ "code": 20125,
+ "name": "PRODTEST_ERR_TROPIC_TEST_SIGN_VERIFY",
+ "module": "tropic"
+ },
+ {
+ "code": 20126,
+ "name": "PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT",
+ "module": "tropic"
+ },
+ {
+ "code": 20127,
+ "name": "PRODTEST_ERR_TROPIC_TEST_COUNTER_GET",
+ "module": "tropic"
+ },
+ {
+ "code": 20128,
+ "name": "PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT_MISMATCH",
+ "module": "tropic"
+ },
+ {
+ "code": 20129,
+ "name": "PRODTEST_ERR_TROPIC_TEST_COUNTER_UPDATE",
+ "module": "tropic"
+ },
+ {
+ "code": 20130,
+ "name": "PRODTEST_ERR_TROPIC_TEST_COUNTER_MISMATCH",
+ "module": "tropic"
+ },
+ {
+ "code": 20131,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RMEM_ERASE",
+ "module": "tropic"
+ },
+ {
+ "code": 20132,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RMEM_WRITE",
+ "module": "tropic"
+ },
+ {
+ "code": 20133,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RMEM_READ",
+ "module": "tropic"
+ },
+ {
+ "code": 20134,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RMEM_MISMATCH",
+ "module": "tropic"
+ },
+ {
+ "code": 20135,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RNG_ZERO",
+ "module": "tropic"
+ },
+ {
+ "code": 20136,
+ "name": "PRODTEST_ERR_TROPIC_TEST_RNG_REPEAT",
+ "module": "tropic"
+ },
+ {
+ "code": 20137,
+ "name": "PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_COUNT",
+ "module": "tropic"
+ },
+ {
+ "code": 20138,
+ "name": "PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_RANGE",
+ "module": "tropic"
+ },
{
"code": 21010,
"name": "PRODTEST_ERR_UNIT_TEST_FAILED",
diff --git a/core/embed/projects/prodtest/prodtest_error_codes.h b/core/embed/projects/prodtest/prodtest_error_codes.h
index 80467570..c41e58d8 100644
--- a/core/embed/projects/prodtest/prodtest_error_codes.h
+++ b/core/embed/projects/prodtest/prodtest_error_codes.h
@@ -378,6 +378,25 @@ typedef enum {
PRODTEST_ERR_TROPIC_BENCHMARK_MCOUNTER_GET = 20117,
PRODTEST_ERR_TROPIC_BENCHMARK_MCOUNTER_UPDATE = 20118,
PRODTEST_ERR_TROPIC_BENCHMARK_RANDOM_GET = 20119,
+ PRODTEST_ERR_TROPIC_STRESS_MAC_AND_DESTROY = 20120,
+ PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY = 20121,
+ PRODTEST_ERR_TROPIC_TEST_MAC_AND_DESTROY_MISMATCH = 20122,
+ PRODTEST_ERR_TROPIC_TEST_SIGN_KEY_READ = 20123,
+ PRODTEST_ERR_TROPIC_TEST_SIGN_CURVE = 20124,
+ PRODTEST_ERR_TROPIC_TEST_SIGN_VERIFY = 20125,
+ PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT = 20126,
+ PRODTEST_ERR_TROPIC_TEST_COUNTER_GET = 20127,
+ PRODTEST_ERR_TROPIC_TEST_COUNTER_INIT_MISMATCH = 20128,
+ PRODTEST_ERR_TROPIC_TEST_COUNTER_UPDATE = 20129,
+ PRODTEST_ERR_TROPIC_TEST_COUNTER_MISMATCH = 20130,
+ PRODTEST_ERR_TROPIC_TEST_RMEM_ERASE = 20131,
+ PRODTEST_ERR_TROPIC_TEST_RMEM_WRITE = 20132,
+ PRODTEST_ERR_TROPIC_TEST_RMEM_READ = 20133,
+ PRODTEST_ERR_TROPIC_TEST_RMEM_MISMATCH = 20134,
+ PRODTEST_ERR_TROPIC_TEST_RNG_ZERO = 20135,
+ PRODTEST_ERR_TROPIC_TEST_RNG_REPEAT = 20136,
+ PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_COUNT = 20137,
+ PRODTEST_ERR_TROPIC_EXPLICIT_SLOT_RANGE = 20138,
// === unit-test (21000–21999) ===
PRODTEST_ERR_UNIT_TEST_FAILED = 21010,
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.