release: pin RAK3401 b40 mOTA chain
This commit is contained in:
1 parent
b40d2e6cb2
commit
f46262dadc
3 files changed
+348
-127
No files matched your search
+138
-76
@@ -1,32 +1,32 @@
|
||||
# RAK3401 1W repeater compact LoRa update chain
|
||||
|
||||
> Status: **unreleased candidate with physically passed package transitions.**
|
||||
> On 29-Aug-2026 all ten exact package transitions completed on the target
|
||||
> RAK3401 with its deployed `0.9.2-OTAFIX2.4` bootloader and a Heltec V4 source at
|
||||
> 909.950 MHz / 500 kHz / SF5 / CR5. Every intermediate EndF hash matched.
|
||||
> Independent SWD readback then matched the endpoint application, original
|
||||
> bootloader, SoftDevice/MBR, and UICR byte-for-byte. The host runner received
|
||||
> cleanup/recovery fixes after that test and has not had a new clean end-to-end
|
||||
> physical rerun. Multi-hop and alternate-bandwidth estimates remain planning
|
||||
> data, not physical qualification.
|
||||
> Status: **published prerelease for controlled, recoverable lab use.** On
|
||||
> 29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte
|
||||
> identical to the corresponding transitions in the physically passed
|
||||
> `fd98bc90` chain. The replacement step 10 installs the current `b40d2e6c`
|
||||
> endpoint. That replacement package and the hardened current runner have
|
||||
> passed offline reconstruction plus both required bootloader simulators, but
|
||||
> have not had a new physical end-to-end run. Use the lab gate and keep local
|
||||
> DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates
|
||||
> remain planning data, not physical qualification.
|
||||
|
||||
The candidate is a local qualification artifact, not a GitHub release:
|
||||
The prerelease is
|
||||
[`rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c`](https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c).
|
||||
Use this asset:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
|
||||
```
|
||||
|
||||
- candidate ZIP SHA-256: `c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881`
|
||||
- endpoint image SHA-256: `31c182c888ceb1135e5afb2376610d93cee2e807b556c838e07fd4486c79d095`
|
||||
- endpoint EndF body hash: `9179B98432895924`
|
||||
- prerelease ZIP SHA-256: `8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a`
|
||||
- endpoint image SHA-256: `a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905`
|
||||
- endpoint EndF body hash: `1FA6AAA3C28D8BD8`
|
||||
|
||||
Always pass the exact local path with `--bundle`. The runner has no download
|
||||
URL for this candidate. `--verify-only` remains the safe default. The hidden
|
||||
`--accept-test-candidate` override is intentionally absent from `--help` and
|
||||
remains required until this exact artifact is published with its qualification
|
||||
record; direct physical success does not silently turn a local file into a
|
||||
released deployment asset.
|
||||
`--verify-only` remains the safe default. Live preflight and installation of
|
||||
this prerelease require `--accept-test-candidate`. That switch bypasses only
|
||||
the prerelease-status gate; it does not bypass the archive, identity,
|
||||
bootloader, route, watchdog, reachability, or post-boot checks.
|
||||
|
||||
## Exact destination
|
||||
|
||||
@@ -37,8 +37,8 @@ target:
|
||||
- hardware: `RAK_3401`
|
||||
- role: `RAK_3401_repeater_lora_ota_no_external_sensors`
|
||||
- start: `v1.16.7.0-c1caa5ad`, EndF `71F4026CBE4B8B74`
|
||||
- endpoint source: MeshCore `fd98bc90f7fb25d5956facd550e227b217abbe95`
|
||||
- endpoint label: `v1.17.1.5-halo-keymind-cascade-dev-fd98bc90`
|
||||
- endpoint source: MeshCore `b40d2e6cb2a185db740175e8b161ea71736d19d9`
|
||||
- endpoint label: `v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c`
|
||||
- endpoint packed version: `0x01110105`
|
||||
- deployment target key: `63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d`
|
||||
|
||||
@@ -86,14 +86,14 @@ Every package in this candidate is an ordinary application container using
|
||||
format ABI 2 and in-place codec 2. The historical bridge applications scan the
|
||||
bootloader capability marker byte-by-byte, so they recognize the released
|
||||
Preview 5 RAK4631 marker at absolute address `0xFCDEA` even though it is two
|
||||
bytes off a word boundary. The `fd98bc90` endpoint deliberately retains that
|
||||
exact Preview 5 application-update fallback. Its separately compiled internal
|
||||
bytes off a word boundary. The `b40d2e6c` endpoint retains that exact Preview
|
||||
5 application-update fallback. Its separately compiled internal
|
||||
bootloader-update feature remains unavailable on Preview 5 because privileged
|
||||
self-update requires a current ABI 3 marker with the exact storage profile.
|
||||
Installing this chain therefore does not replace the bootloader, and the final
|
||||
application can still accept a future valid ABI-2/codec-2 application update.
|
||||
|
||||
The schema-2 exhaustive search checked 17,402 candidate geometries and found
|
||||
The schema-2 exhaustive search checked 17,371 candidate geometries and found
|
||||
121 feasible forward edges in the declared 33-image inventory. Fourteen
|
||||
ten-package routes tied for the minimum package count; the selected route has
|
||||
the smallest total container size among them. That minimum applies to the
|
||||
@@ -111,12 +111,21 @@ is:
|
||||
| 7 | 1.16.9.113 | 1.16.9.117 | `0x8B000` | 139,637 | 0 |
|
||||
| 8 | 1.16.9.117 | 1.16.10.0 | `0x7E000` | 174,202 | 20,480 |
|
||||
| 9 | 1.16.10.0 | 1.17.1.3 | `0x6B000` | 271,569 | 0 |
|
||||
| 10 | 1.17.1.3 | 1.17.1.5 | `0x74000` | 173,334 | 61,440 |
|
||||
| 10 | 1.17.1.3 | 1.17.1.5 | `0x74000` | 174,744 | 61,440 |
|
||||
|
||||
Total mOTA transfer data is 1,382,842 bytes. `ROUTE.json`, `CHAIN.csv`, and
|
||||
Total mOTA transfer data is 1,384,252 bytes. `ROUTE.json`, `CHAIN.csv`, and
|
||||
`validation-results.json` in the bundle pin the exact geometry and image hash
|
||||
for every transition.
|
||||
|
||||
Steps 1-9 are byte-for-byte identical to the transitions that passed on the
|
||||
physical RAK3401. The replacement step 10, from the physically reached
|
||||
`FE65A6135A1E7B3F` v1.17.1.3 body to `1FA6AAA3C28D8BD8`, has offline
|
||||
qualification only. All ten packages passed independent container checks,
|
||||
zero-filled and erased-workspace reconstruction, the OTAFIX 2.4.1 Preview 5
|
||||
simulator, and the current OTAFIX simulator. Those results prove the package
|
||||
bytes and apply geometry; they are not a physical claim for the replacement
|
||||
step 10 or the hardened runner.
|
||||
|
||||
## Host requirements
|
||||
|
||||
Install Python 3.9 or newer, `meshcli` 1.6.0 or newer, the current `motatool`,
|
||||
@@ -128,7 +137,7 @@ No password or device is needed:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--motatool /path/to/motatool \
|
||||
--verify-only
|
||||
@@ -141,14 +150,14 @@ inner coverage, all ten manifests, continuity, image anchors, both recovery
|
||||
images, and every container with `motatool`. Passing this command means offline
|
||||
and simulator qualification only; it does not unlock live use.
|
||||
|
||||
## Current guarded reproduction command
|
||||
## Guarded prerelease lab command
|
||||
|
||||
These are the same direct-link arguments used for the physical transition run,
|
||||
but the current host runner also contains later cleanup and recovery fixes.
|
||||
That revised runner has not been cleanly rerun through all ten transitions.
|
||||
Keep local USB/SWD recovery available while the artifact remains unreleased.
|
||||
The hidden override bypasses only the candidate-status block; all identity,
|
||||
checksum, bootloader, route, watchdog, and post-boot gates still run.
|
||||
These direct-link arguments match the physical transition setup, but the
|
||||
current package replaces step 10 and the current host runner contains later
|
||||
cleanup and recovery fixes. Neither has been cleanly rerun through the whole
|
||||
chain. Keep local USB/SWD recovery available. The candidate switch bypasses
|
||||
only the prerelease-status block; all identity, checksum, bootloader, route,
|
||||
watchdog, reachability, and post-boot gates still run.
|
||||
|
||||
Restore the test start locally with
|
||||
`recovery/test-start/RAK3401-test-start-v1.16.7-c1caa5ad.uf2`, then run:
|
||||
@@ -157,7 +166,7 @@ Restore the test start locally with
|
||||
export MESHCORE_ADMIN_PASSWORD='password'
|
||||
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp 192.168.1.51:5001 \
|
||||
@@ -179,7 +188,30 @@ zero and restores it at the endpoint. Use that option only where the selected
|
||||
frequency and local duty-cycle rules permit a full transmit budget; omit it
|
||||
otherwise.
|
||||
|
||||
### Measured direct-link timing
|
||||
## Legacy `fd98bc90` physical and SWD qualification
|
||||
|
||||
The earlier local candidate remains the exact physical evidence source for
|
||||
this route:
|
||||
|
||||
```text
|
||||
RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
```
|
||||
|
||||
- candidate ZIP SHA-256: `c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61`
|
||||
- inner `SHA256SUMS.txt` SHA-256: `3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881`
|
||||
- endpoint image SHA-256: `31c182c888ceb1135e5afb2376610d93cee2e807b556c838e07fd4486c79d095`
|
||||
- endpoint EndF body hash: `9179B98432895924`
|
||||
|
||||
All ten exact `fd98bc90` transitions completed on the RAK3401 with its
|
||||
deployed `0.9.2-OTAFIX2.4` bootloader and a Heltec V4 source at 909.950 MHz /
|
||||
500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD
|
||||
readback then matched the endpoint application, original bootloader,
|
||||
SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine
|
||||
packages in the `b40d2e6c` prerelease. The two candidates have different step
|
||||
10 packages and endpoints, so the physical result and step-10 timing must not
|
||||
be attributed to `b40d2e6c`.
|
||||
|
||||
### Measured direct-link timing for `fd98bc90`
|
||||
|
||||
The first `ready to install` transfer ages were:
|
||||
|
||||
@@ -196,23 +228,44 @@ The first `ready to install` transfer ages were:
|
||||
| 9 | 264 | 187 s |
|
||||
| 10 | 169 | 193 s |
|
||||
|
||||
The physically observed bulk-transfer total for those exact packages is 1,605
|
||||
seconds (26m45s). The log came from the pre-cleanup-fix runner revision. Step 1
|
||||
was measured before the host began forcing source RXPS off and is therefore a
|
||||
conservative outlier. A new complete run with that fix is projected at roughly
|
||||
18–19 minutes of bulk transfer, but that projection is not a substitute for
|
||||
the measured total. Allow about 70–75 minutes direct at BW500 for validation,
|
||||
ten transfers, installs/reboots, retained-store checks, and final restoration.
|
||||
The physically observed bulk-transfer total for those exact legacy packages is
|
||||
1,605 seconds (26m45s). The log came from the pre-cleanup-fix runner revision.
|
||||
Step 1 was measured before the host began forcing source RXPS off and is
|
||||
therefore a conservative outlier. A new complete run with that fix is projected
|
||||
at roughly 18–19 minutes of bulk transfer, but that projection is not a
|
||||
substitute for the measured total. Allow about 70–75 minutes direct at BW500
|
||||
for validation, ten transfers, installs/reboots, retained-store checks, and
|
||||
final restoration.
|
||||
|
||||
Step 10's longer per-block time was not a V4 TempRadio delay: the source RXPS
|
||||
readback was off. Its v1.17.1.3 requester uses non-overlapping adaptive flights
|
||||
and a five-second recovery floor and logged more block reloads than step 8.
|
||||
The V4 itself intentionally schedules its local radio handoff after 1.5 seconds
|
||||
so the normal-channel command reply can drain; the host waits three seconds and
|
||||
then checks the live TempRadio state.
|
||||
The legacy step 10's longer per-block time was not a V4 TempRadio delay: the
|
||||
source RXPS readback was off. Its v1.17.1.3 requester uses non-overlapping
|
||||
adaptive flights and a five-second recovery floor and logged more block reloads
|
||||
than step 8. The V4 itself intentionally schedules its local radio handoff
|
||||
after 1.5 seconds so the normal-channel command reply can drain; the host waits
|
||||
three seconds and then checks the live TempRadio state.
|
||||
|
||||
For planning only, scale the conservative measured 26m45s bulk baseline by the
|
||||
qualified adaptive-preamble airtime and by each additional relay transmission:
|
||||
### Live progress and speed sanity checks
|
||||
|
||||
The runner watches `motatool serve -v` locally, so its quarter-progress reports
|
||||
do not consume LoRa airtime. At BW500 direct, physically measured steps 2–9
|
||||
averaged about 1.1–1.4 newly requested payload blocks per second; the legacy
|
||||
step 10 averaged about 0.88 blocks/s, and step 1's 0.15 blocks/s was the known
|
||||
pre-RXPS-fix outlier. Use those only as bench sanity ranges, not guarantees.
|
||||
Relays, narrower bandwidths, interference, and recovery reads lower the rate.
|
||||
|
||||
Healthy output keeps advancing from `[download] passive source progress x/y`
|
||||
to `source read all y payload blocks`, followed by destination status `ready to
|
||||
install y/y (100%)` for the same manifest ID. Aggregate reads above the unique
|
||||
block count indicate recovery/retry traffic. A radio-scaled no-progress window
|
||||
causes one destination status check; the transfer timeout leaves the partial
|
||||
download staged for an exact-package resume. Stop and diagnose instead of
|
||||
installing if the manifest changes, progress remains flat through those checks,
|
||||
the final receiver count is not `y/y`, or the measured rate is far below the
|
||||
matching bandwidth/hop estimate without an understood RF cause.
|
||||
|
||||
For planning only, scale the conservative measured `fd98bc90` 26m45s bulk
|
||||
baseline by the qualified adaptive-preamble airtime and by each additional
|
||||
relay transmission:
|
||||
|
||||
| Bandwidth, SF5/CR5 | Direct / 0 relays | 1 relay | 2 relays |
|
||||
|---:|---:|---:|---:|
|
||||
@@ -238,17 +291,17 @@ never skip a step even when a later package appears in `ota ls`.
|
||||
### 1. Verify and extract the asset
|
||||
|
||||
```bash
|
||||
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip
|
||||
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90
|
||||
sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
|
||||
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
|
||||
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c
|
||||
sha256sum -c SHA256SUMS.txt
|
||||
for package in motas/*.mota; do motatool verify "$package" || exit 1; done
|
||||
```
|
||||
|
||||
The outer hash must be
|
||||
`c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61`,
|
||||
`8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4`,
|
||||
and the SHA-256 of the extracted `SHA256SUMS.txt` must be
|
||||
`3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881`.
|
||||
`29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a`.
|
||||
Do not continue after any checksum or verification error.
|
||||
|
||||
### 2. Record and prepare the destination
|
||||
@@ -342,7 +395,7 @@ to install`.
|
||||
| 7 | `80F40DB6` | `65328FC8A1FBED2D` | `1.16.9.117` | `AF7532E13337FADD` |
|
||||
| 8 | `DBF6310D` | `AF7532E13337FADD` | `1.16.10.0` | `22002359BBDFA76E` |
|
||||
| 9 | `C2979E08` | `22002359BBDFA76E` | `1.17.1.3` | `FE65A6135A1E7B3F` |
|
||||
| 10 | `319DEC92` | `FE65A6135A1E7B3F` | `1.17.1.5` | `9179B98432895924` |
|
||||
| 10 | `B5011420` | `FE65A6135A1E7B3F` | `1.17.1.5` | `1FA6AAA3C28D8BD8` |
|
||||
|
||||
For each row, first prove that `ota self` exactly matches its **Before hash**,
|
||||
then inspect the manager before changing it:
|
||||
@@ -406,10 +459,9 @@ If the board boots but `ota self` says the EndF is invalid, stop. Only when
|
||||
`ota status` still identifies that row's complete staged package may the
|
||||
guarded `ota rescue install BEFORE_HASH` command be used. It is not a force
|
||||
option and must use that row's exact Before hash. If the board does not boot,
|
||||
recover the documented start/recovery UF2 locally over USB. The fd98 endpoint
|
||||
deliberately disables rescue on its shared internal bootloader-update build;
|
||||
if its EndF is invalid, use local DFU/SWD rather than attempting another LoRa
|
||||
package.
|
||||
recover the documented start/recovery UF2 locally over USB. Step 10 is the
|
||||
endpoint and has no successor package in this chain. If its EndF is invalid,
|
||||
use local DFU/SWD rather than attempting an unqualified LoRa package.
|
||||
|
||||
### 4. Intermediate relays and restoration
|
||||
|
||||
@@ -436,7 +488,7 @@ the saved tuple after replying on the temporary channel:
|
||||
normalradio
|
||||
```
|
||||
|
||||
The fd98 endpoint supports that command. If a recovery build unexpectedly
|
||||
The `b40d2e6c` endpoint supports that command. If a recovery build unexpectedly
|
||||
replies `Unknown command`, shorten its lease to one minute with the same tuple:
|
||||
|
||||
```text
|
||||
@@ -456,25 +508,26 @@ A Full Companion source is the exception: its local TCP console supports
|
||||
Binary API radio tuple.
|
||||
|
||||
Final lab success requires version
|
||||
`v1.17.1.5-halo-keymind-cascade-dev-fd98bc90`, body hash
|
||||
`9179B98432895924`, target `2FA509C1`, hardware `RAK_3401`, and
|
||||
`v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c`, body hash
|
||||
`1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, and
|
||||
`get system.watchdog` reporting `> on`. If a relay cannot be restored before
|
||||
its TempRadio lease ends, wait for it to return to the normal channel and
|
||||
restore its saved `rxdelay` and `txdelay` there.
|
||||
|
||||
## Two-relay deployment
|
||||
|
||||
The exact direct package transitions above were physically observed; this
|
||||
post-run host-runner revision and multi-hop operation are not cleanly
|
||||
end-to-end qualified. A controlled, recoverable multi-hop lab can exercise two
|
||||
intermediate relays by
|
||||
The first nine direct package transitions above were physically observed as
|
||||
the byte-identical first nine transitions of the legacy chain. Replacement
|
||||
step 10, this hardened host-runner revision, and multi-hop operation are not
|
||||
cleanly end-to-end physically qualified. A controlled, recoverable multi-hop
|
||||
lab can exercise two intermediate relays by
|
||||
listing them farthest-to-nearest and using three OTA hops. Run a non-mutating
|
||||
preflight first; even preflight requires the hidden candidate override because
|
||||
the artifact remains unreleased and the command connects to live devices:
|
||||
preflight first; even preflight requires the prerelease candidate override
|
||||
because the command connects to live devices:
|
||||
|
||||
```bash
|
||||
python3 tools/lora_ota/rak3401_mota_chain.py \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip \
|
||||
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
|
||||
--work-dir ./rak3401-mota-chain-work \
|
||||
--controller-serial /dev/ttyACM0 \
|
||||
--source-tcp SOURCE_IP:5001 \
|
||||
@@ -501,8 +554,8 @@ step 1 and remains available through the step-9 bridge. It is not a force
|
||||
command: it refuses a valid normal EndF, a base mismatch, wrong
|
||||
target/hardware, or invalid payload. A failure before step 1 completes still
|
||||
requires local USB recovery because the deployed start image predates the
|
||||
rescue command. The final fd98 endpoint uses the stricter shared-slot profile;
|
||||
its invalid-EndF recovery is local DFU/SWD, not the rescue command.
|
||||
rescue command. The final `b40d2e6c` endpoint uses the stricter shared-slot
|
||||
profile; its invalid-EndF recovery is local DFU/SWD, not the rescue command.
|
||||
|
||||
Before the first mutation, the runner saves the destination's RXPS periods,
|
||||
CPU power-saving state, RX flood delay, airtime factor, and OTA hop reach in
|
||||
@@ -537,7 +590,7 @@ the relay leaves TempRadio.
|
||||
The runner also keeps the watchdog off across the chain, rechecks identity and
|
||||
OTA reach before every package, requires the exact post-boot EndF hash, and
|
||||
only re-enables the watchdog after step 10. Lab success requires endpoint body
|
||||
hash `9179B98432895924`, target `2FA509C1`, hardware `RAK_3401`, normal radio
|
||||
hash `1FA6AAA3C28D8BD8`, target `2FA509C1`, hardware `RAK_3401`, normal radio
|
||||
`910.525 MHz / 62.5 kHz / SF7 / CR5`, and the watchdog verified on.
|
||||
|
||||
## External-radio startup and manual USB recovery
|
||||
@@ -573,6 +626,15 @@ the battery as well as USB and reseat the WisBlock module. Remote firmware will
|
||||
continue retrying without requiring that physical intervention or churning the
|
||||
USB device.
|
||||
|
||||
For an existing deployed tower using Preview 5, do not change the bootloader
|
||||
as part of this application chain. Its exact local recovery package is the
|
||||
[Preview 5 RAK4631 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4.1-preview.5/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4.1-preview.5_s140_6.1.1.zip).
|
||||
For a new RAK3401 installation or local recovery, use the
|
||||
[OTAFIX 2.4.3 RAK3401 ZIP](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.11.0-OTAFIX2.4.3/wiscore_rak3401_bootloader-0.11.0-OTAFIX2.4.3_s140_6.1.1.zip)
|
||||
from the
|
||||
[OTAFIX 2.4.3 release](https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/tag/0.11.0-OTAFIX2.4.3).
|
||||
These are local DFU/SWD recovery assets, not mOTA chain steps.
|
||||
|
||||
For a local Serial DFU recovery with the repository helper:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -29,16 +29,36 @@ except ImportError:
|
||||
import lora_ota as ota
|
||||
|
||||
|
||||
# These exact ten package transitions passed directly on the physical target,
|
||||
# and the resulting endpoint passed independent SWD readback. The host runner
|
||||
# has received cleanup/recovery fixes since that run and has not itself had a
|
||||
# new clean end-to-end qualification run. Keep the artifact unpublished and
|
||||
# normally gated from live use; never invent a release URL.
|
||||
RELEASE_URL = "unreleased local RAK3401 10-step candidate"
|
||||
ASSET_NAME = "RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip"
|
||||
ASSET_SHA256 = "c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61"
|
||||
CHECKSUM_LIST_SHA256 = "3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881"
|
||||
BUNDLE_ROOT_NAME = "RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90"
|
||||
# The published b40d2e6c prerelease keeps the first nine package bytes from the
|
||||
# physically passed fd98bc90 chain and replaces its endpoint package. The new
|
||||
# step 10 and this hardened host runner have exhaustive offline/simulator
|
||||
# evidence, but not a clean physical transition run, so live use remains gated.
|
||||
RELEASE_URL = (
|
||||
"https://github.com/mikecarper/MeshCore/releases/tag/"
|
||||
"rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c"
|
||||
)
|
||||
ASSET_NAME = "RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip"
|
||||
ASSET_SHA256 = "8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4"
|
||||
CHECKSUM_LIST_SHA256 = "29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a"
|
||||
BUNDLE_ROOT_NAME = "RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c"
|
||||
|
||||
# Preserve the exact fd98bc90 candidate whose ten package transitions passed
|
||||
# directly on the physical RAK3401 and whose endpoint passed independent SWD
|
||||
# readback. It remains a separately identified, hidden-lab-ack-gated legacy
|
||||
# artifact; sharing its version and first nine targets with b40d2e6c must never
|
||||
# make the two archives interchangeable.
|
||||
PHYSICALLY_PASSED_FD98_ASSET_NAME = (
|
||||
"RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90.zip"
|
||||
)
|
||||
PHYSICALLY_PASSED_FD98_ASSET_SHA256 = (
|
||||
"c0b33f4568985e8b2b8dc99411295907212cf2bad21764b6333d5e0ba298fd61"
|
||||
)
|
||||
PHYSICALLY_PASSED_FD98_CHECKSUM_LIST_SHA256 = (
|
||||
"3f8c4af8096b96a4aa6506825c387cc8a06f74d5213a29c9387bd11689546881"
|
||||
)
|
||||
PHYSICALLY_PASSED_FD98_ROOT_NAME = (
|
||||
"RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-fd98bc90"
|
||||
)
|
||||
|
||||
# Preserve the physically qualified nine-step chain for exact offline
|
||||
# recognition and existing live deployments.
|
||||
@@ -140,6 +160,9 @@ MAX_CHECKSUM_LIST_BYTES = 1024 * 1024
|
||||
MAX_PROGRESS_BYTES = 1024 * 1024
|
||||
PINNED_ARCHIVE_CHECKSUMS = {
|
||||
ASSET_SHA256: CHECKSUM_LIST_SHA256,
|
||||
PHYSICALLY_PASSED_FD98_ASSET_SHA256: (
|
||||
PHYSICALLY_PASSED_FD98_CHECKSUM_LIST_SHA256
|
||||
),
|
||||
PHYSICALLY_QUALIFIED_9_ASSET_SHA256: (
|
||||
PHYSICALLY_QUALIFIED_9_CHECKSUM_LIST_SHA256
|
||||
),
|
||||
@@ -241,11 +264,10 @@ COMPACT_RELEASE_ANCHORS = (
|
||||
(9, "2784e4b645bc3dc198de0b8b18d3d7369cd02eca61cd71c46a51b61854da5345"),
|
||||
)
|
||||
|
||||
# Every target in the exact unreleased ten-package fd98bc90 sequence that
|
||||
# passed direct physical transitions. These pins bind both offline verification
|
||||
# and the hidden explicit live-lab override to those exact package bytes; they
|
||||
# do not qualify later host-runner changes.
|
||||
CURRENT_10_CANDIDATE_ANCHORS = (
|
||||
# Every target in the exact legacy fd98bc90 sequence whose package transitions
|
||||
# passed directly on the physical RAK3401. The b40d2e6c prerelease reuses the
|
||||
# first nine package bytes exactly, but has its own endpoint pin below.
|
||||
PHYSICALLY_PASSED_FD98_ANCHORS = (
|
||||
(1, "8364257a2b3a219905e870fad6fbb2040a96ca4b4bb7201b2867534cc2b45530"),
|
||||
(2, "ac5f50e5028378ccfe6ea08bbf32f227f50fdcf5285a7deb866e309fbdd0a88f"),
|
||||
(3, "884b5e9355b4585b7a4e079dbb44d2858ec46e2fa7bb0dba47e810db2a82e349"),
|
||||
@@ -258,6 +280,11 @@ CURRENT_10_CANDIDATE_ANCHORS = (
|
||||
(10, "31c182c888ceb1135e5afb2376610d93cee2e807b556c838e07fd4486c79d095"),
|
||||
)
|
||||
|
||||
CURRENT_10_CANDIDATE_ANCHORS = (
|
||||
*PHYSICALLY_PASSED_FD98_ANCHORS[:9],
|
||||
(10, "a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905"),
|
||||
)
|
||||
|
||||
# Exact anchors for every image in the accelerated 30-step release. The outer
|
||||
# asset and inner checksum-list pins cover every byte as well; the complete
|
||||
# anchor table keeps structural validation fail-closed if those layers are
|
||||
@@ -336,31 +363,86 @@ SUPERSEDED_30_MESSAGE = (
|
||||
"the deployed bootloader. Use --verify-only for the older bundle."
|
||||
)
|
||||
CURRENT_10_CANDIDATE_MESSAGE = (
|
||||
"live installation of the exact fd98bc90 ten-step candidate is disabled: "
|
||||
"the ten pinned package transitions completed directly on the physical "
|
||||
"RAK3401 and the endpoint passed independent SWD readback, but the current "
|
||||
"host runner includes later cleanup/recovery fixes that have not had a new "
|
||||
"clean end-to-end physical run. The artifact is also local and unpublished. "
|
||||
"Use --verify-only with the explicit local bundle path, or the hidden "
|
||||
"controlled-lab override, until the exact artifact and qualification record "
|
||||
"are published."
|
||||
"live installation of the exact published b40d2e6c ten-step prerelease is "
|
||||
"disabled: steps 1-9 are byte-identical to package transitions that "
|
||||
"completed directly on the physical RAK3401, but the new b40d2e6c step 10 "
|
||||
"and this hardened host runner have passed offline reconstruction, motatool "
|
||||
"verification, and both bootloader simulators only. The exact final "
|
||||
"transition has not had a clean physical run. Use --verify-only, or the "
|
||||
"hidden controlled-lab override, until that qualification is complete."
|
||||
)
|
||||
PHYSICALLY_PASSED_FD98_MESSAGE = (
|
||||
"live installation of the exact legacy fd98bc90 ten-step candidate is "
|
||||
"disabled: all ten pinned package transitions completed directly on the "
|
||||
"physical RAK3401 and the endpoint passed independent SWD readback, but this "
|
||||
"later hardened host runner has not had a clean end-to-end physical run. "
|
||||
"Use --verify-only with the explicit legacy bundle path, or the hidden "
|
||||
"controlled-lab override."
|
||||
)
|
||||
|
||||
|
||||
class KnownUnsafeReleaseError(ota.OtaError):
|
||||
"""The pinned artifacts are intact but their live transition is unsafe."""
|
||||
|
||||
|
||||
def matches_exact_anchors(
|
||||
steps: list[ChainStep],
|
||||
anchors: tuple[tuple[int, str], ...],
|
||||
) -> bool:
|
||||
return len(steps) == len(anchors) and all(
|
||||
number == position
|
||||
and steps[position - 1].target_sha256 == expected_sha256
|
||||
for position, (number, expected_sha256) in enumerate(anchors, 1)
|
||||
)
|
||||
|
||||
|
||||
def ten_step_candidate_kind(steps: list[ChainStep]) -> str | None:
|
||||
if matches_exact_anchors(steps, CURRENT_10_CANDIDATE_ANCHORS):
|
||||
return "current-b40d2e6c"
|
||||
if matches_exact_anchors(steps, PHYSICALLY_PASSED_FD98_ANCHORS):
|
||||
return "legacy-fd98bc90"
|
||||
return None
|
||||
|
||||
|
||||
def ten_step_verification_message(steps: list[ChainStep]) -> str:
|
||||
candidate_kind = ten_step_candidate_kind(steps)
|
||||
if candidate_kind == "current-b40d2e6c":
|
||||
return (
|
||||
"Current b40d2e6c ten-step prerelease verified offline: exact "
|
||||
"archive/checksum pins, all target anchors, zero-filled and "
|
||||
"erased-workspace reconstruction, independent motatool verification, "
|
||||
"and both bootloader simulations passed. Steps 1-9 are byte-identical "
|
||||
"to packages that passed directly on the RAK3401. The new step 10 and "
|
||||
"this hardened host runner have not had a clean physical run; live "
|
||||
"use remains gated."
|
||||
)
|
||||
if candidate_kind == "legacy-fd98bc90":
|
||||
return (
|
||||
"Legacy fd98bc90 ten-step candidate verified offline: exact "
|
||||
"archive/checksum pins, all target anchors, zero-filled and "
|
||||
"erased-workspace reconstruction, independent motatool verification, "
|
||||
"and bootloader simulation passed. These exact ten package "
|
||||
"transitions also passed directly on the RAK3401 and the endpoint "
|
||||
"passed independent SWD readback. This later hardened host runner has "
|
||||
"not had a clean physical rerun; live use remains gated."
|
||||
)
|
||||
raise ota.OtaError("unrecognized ten-step candidate reached verification report")
|
||||
|
||||
|
||||
def require_live_release_safe(
|
||||
args: argparse.Namespace,
|
||||
steps: list[ChainStep],
|
||||
) -> None:
|
||||
if len(steps) == EXPECTED_STEP_COUNT:
|
||||
if all(
|
||||
steps[number - 1].target_sha256 == expected_sha256
|
||||
for number, expected_sha256 in CURRENT_10_CANDIDATE_ANCHORS
|
||||
):
|
||||
candidate_kind = ten_step_candidate_kind(steps)
|
||||
if candidate_kind == "current-b40d2e6c":
|
||||
if args.accept_test_candidate:
|
||||
return
|
||||
raise KnownUnsafeReleaseError(CURRENT_10_CANDIDATE_MESSAGE)
|
||||
if candidate_kind == "legacy-fd98bc90":
|
||||
if args.accept_test_candidate:
|
||||
return
|
||||
raise KnownUnsafeReleaseError(PHYSICALLY_PASSED_FD98_MESSAGE)
|
||||
raise KnownUnsafeReleaseError(
|
||||
"live installation is disabled: this is an unrecognized variant "
|
||||
"of the pinned ten-step candidate"
|
||||
@@ -615,8 +697,8 @@ def download_release_asset(destination: Path) -> None:
|
||||
print(f"[bundle] using verified cached asset {destination}")
|
||||
return
|
||||
raise ota.OtaError(
|
||||
"the exact ten-step candidate is not released; pass its explicit local "
|
||||
"ZIP or extracted root with --bundle"
|
||||
f"automatic release download is disabled; download {ASSET_NAME} from "
|
||||
f"{RELEASE_URL} and pass its ZIP or extracted root with --bundle"
|
||||
)
|
||||
|
||||
|
||||
@@ -926,6 +1008,7 @@ def locate_bundle(args: argparse.Namespace, work_dir: Path) -> Path:
|
||||
)
|
||||
if actual not in {
|
||||
ASSET_SHA256,
|
||||
PHYSICALLY_PASSED_FD98_ASSET_SHA256,
|
||||
PHYSICALLY_QUALIFIED_9_ASSET_SHA256,
|
||||
SUPERSEDED_30_ASSET_SHA256,
|
||||
SUPERSEDED_29_ASSET_SHA256,
|
||||
@@ -950,6 +1033,7 @@ def read_checksum_entries(bundle_root: Path) -> dict[PurePosixPath, str]:
|
||||
checksum_digest = hashlib.sha256(checksum_bytes).hexdigest()
|
||||
expected_lists = {
|
||||
CHECKSUM_LIST_SHA256,
|
||||
PHYSICALLY_PASSED_FD98_CHECKSUM_LIST_SHA256,
|
||||
PHYSICALLY_QUALIFIED_9_CHECKSUM_LIST_SHA256,
|
||||
SUPERSEDED_30_CHECKSUM_LIST_SHA256,
|
||||
SUPERSEDED_29_CHECKSUM_LIST_SHA256,
|
||||
@@ -1246,11 +1330,12 @@ def parse_chain(bundle_root: Path) -> tuple[list[ChainStep], bytes]:
|
||||
if steps[0].from_version != EXPECTED_START_VERSION:
|
||||
raise ota.OtaError("chain has an unexpected starting version")
|
||||
if len(steps) == EXPECTED_STEP_COUNT:
|
||||
for number, expected_sha256 in CURRENT_10_CANDIDATE_ANCHORS:
|
||||
if steps[number - 1].target_sha256 != expected_sha256:
|
||||
raise ota.OtaError(
|
||||
f"ten-step candidate step {number} does not match its audited image pin"
|
||||
)
|
||||
candidate_kind = ten_step_candidate_kind(steps)
|
||||
if candidate_kind is None:
|
||||
raise ota.OtaError(
|
||||
"ten-step candidate does not match the exact b40d2e6c or "
|
||||
"legacy fd98bc90 audited image pins"
|
||||
)
|
||||
expected_final_version = EXPECTED_FINAL_VERSION
|
||||
elif len(steps) == PHYSICALLY_QUALIFIED_9_STEP_COUNT:
|
||||
for number, expected_sha256 in COMPACT_RELEASE_ANCHORS:
|
||||
@@ -2241,16 +2326,20 @@ def run_step(
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Verify the exact local ten-step RAK3401 candidate from c1caa5ad "
|
||||
"to fd98bc90. Its exact package transitions passed directly on "
|
||||
"hardware, but this later host-runner revision has not had a clean "
|
||||
"end-to-end physical rerun and normal live use remains disabled."
|
||||
"Verify the exact published ten-step RAK3401 prerelease from "
|
||||
"c1caa5ad to b40d2e6c. Its first nine package bytes passed directly "
|
||||
"on hardware; the new final transition and this hardened host runner "
|
||||
"have offline/simulator qualification only, so live use remains "
|
||||
"disabled without the controlled-lab override."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bundle",
|
||||
type=Path,
|
||||
help="pinned local ZIP or extracted bundle root (required until release)",
|
||||
help=(
|
||||
"pinned prerelease ZIP or extracted bundle root "
|
||||
"(required; automatic download is disabled)"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--work-dir",
|
||||
@@ -2510,15 +2599,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if args.verify_only:
|
||||
print(f"Verified pinned bundle: {supplied_bundle_root}")
|
||||
if len(steps) == EXPECTED_STEP_COUNT:
|
||||
print(
|
||||
"Ten-step candidate verified offline: exact archive/checksum "
|
||||
"pins, all target anchors, zero-filled and erased-workspace "
|
||||
"reconstruction, independent motatool verification, and "
|
||||
"bootloader simulation passed. These exact package transitions "
|
||||
"also passed directly on the RAK3401 and the endpoint passed "
|
||||
"independent SWD readback. This later host-runner revision has "
|
||||
"not had a new clean physical rerun; live use remains gated."
|
||||
)
|
||||
print(ten_step_verification_message(steps))
|
||||
elif len(steps) == PHYSICALLY_QUALIFIED_9_STEP_COUNT:
|
||||
print(
|
||||
"Compact release verified: all 9 transitions passed its exact "
|
||||
|
||||
@@ -3983,7 +3983,7 @@ class Rak3401KnownUnsafeReleaseTests(unittest.TestCase):
|
||||
steps[number - 1].target_sha256 = image_sha256
|
||||
with self.assertRaisesRegex(
|
||||
rak_chain.KnownUnsafeReleaseError,
|
||||
"package transitions completed directly on the physical RAK3401",
|
||||
"new b40d2e6c step 10.*bootloader simulators only",
|
||||
):
|
||||
rak_chain.require_live_release_safe(
|
||||
argparse.Namespace(accept_test_candidate=False), steps
|
||||
@@ -3992,6 +3992,52 @@ class Rak3401KnownUnsafeReleaseTests(unittest.TestCase):
|
||||
argparse.Namespace(accept_test_candidate=True), steps
|
||||
)
|
||||
|
||||
def test_exact_fd98_ten_step_candidate_remains_gated_legacy(self) -> None:
|
||||
steps = [mock.Mock(target_sha256="") for _ in range(10)]
|
||||
for number, image_sha256 in rak_chain.PHYSICALLY_PASSED_FD98_ANCHORS:
|
||||
steps[number - 1].target_sha256 = image_sha256
|
||||
with self.assertRaisesRegex(
|
||||
rak_chain.KnownUnsafeReleaseError,
|
||||
"legacy fd98bc90.*all ten pinned package transitions completed",
|
||||
):
|
||||
rak_chain.require_live_release_safe(
|
||||
argparse.Namespace(accept_test_candidate=False), steps
|
||||
)
|
||||
rak_chain.require_live_release_safe(
|
||||
argparse.Namespace(accept_test_candidate=True), steps
|
||||
)
|
||||
|
||||
def test_current_and_legacy_ten_step_reports_do_not_mix_evidence(self) -> None:
|
||||
current = [mock.Mock(target_sha256="") for _ in range(10)]
|
||||
legacy = [mock.Mock(target_sha256="") for _ in range(10)]
|
||||
for number, image_sha256 in rak_chain.CURRENT_10_CANDIDATE_ANCHORS:
|
||||
current[number - 1].target_sha256 = image_sha256
|
||||
for number, image_sha256 in rak_chain.PHYSICALLY_PASSED_FD98_ANCHORS:
|
||||
legacy[number - 1].target_sha256 = image_sha256
|
||||
|
||||
current_message = rak_chain.ten_step_verification_message(current)
|
||||
legacy_message = rak_chain.ten_step_verification_message(legacy)
|
||||
self.assertIn("new step 10", current_message)
|
||||
self.assertIn("not had a clean physical run", current_message)
|
||||
self.assertNotIn("endpoint passed independent SWD readback", current_message)
|
||||
self.assertIn("exact ten package transitions", legacy_message)
|
||||
self.assertIn("endpoint passed independent SWD readback", legacy_message)
|
||||
|
||||
def test_ten_step_candidates_share_only_the_pinned_physical_prefix(self) -> None:
|
||||
self.assertEqual(
|
||||
rak_chain.CURRENT_10_CANDIDATE_ANCHORS[:9],
|
||||
rak_chain.PHYSICALLY_PASSED_FD98_ANCHORS[:9],
|
||||
)
|
||||
self.assertNotEqual(
|
||||
rak_chain.CURRENT_10_CANDIDATE_ANCHORS[-1],
|
||||
rak_chain.PHYSICALLY_PASSED_FD98_ANCHORS[-1],
|
||||
)
|
||||
steps = [mock.Mock(target_sha256="") for _ in range(10)]
|
||||
for number, image_sha256 in rak_chain.CURRENT_10_CANDIDATE_ANCHORS:
|
||||
steps[number - 1].target_sha256 = image_sha256
|
||||
steps[8].target_sha256 = "00" * 32
|
||||
self.assertIsNone(rak_chain.ten_step_candidate_kind(steps))
|
||||
|
||||
def test_changed_current_ten_step_anchor_is_not_recognized(self) -> None:
|
||||
steps = [mock.Mock(target_sha256="") for _ in range(10)]
|
||||
for number, image_sha256 in rak_chain.CURRENT_10_CANDIDATE_ANCHORS:
|
||||
@@ -4005,14 +4051,46 @@ class Rak3401KnownUnsafeReleaseTests(unittest.TestCase):
|
||||
argparse.Namespace(accept_test_candidate=True), steps
|
||||
)
|
||||
|
||||
def test_unreleased_candidate_has_no_implicit_download(self) -> None:
|
||||
def test_published_candidate_has_no_implicit_download(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
destination = Path(directory) / rak_chain.ASSET_NAME
|
||||
with self.assertRaisesRegex(
|
||||
ota.OtaError, "not released.*--bundle"
|
||||
ota.OtaError, "automatic release download is disabled.*--bundle"
|
||||
):
|
||||
rak_chain.download_release_asset(destination)
|
||||
|
||||
def test_fd98_archive_and_inner_checksum_remain_pinned(self) -> None:
|
||||
self.assertEqual(
|
||||
rak_chain.PINNED_ARCHIVE_CHECKSUMS[
|
||||
rak_chain.PHYSICALLY_PASSED_FD98_ASSET_SHA256
|
||||
],
|
||||
rak_chain.PHYSICALLY_PASSED_FD98_CHECKSUM_LIST_SHA256,
|
||||
)
|
||||
|
||||
def test_locate_bundle_accepts_exact_fd98_legacy_archive(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
archive = root / rak_chain.PHYSICALLY_PASSED_FD98_ASSET_NAME
|
||||
archive.write_bytes(b"fixture is identified by the mocked digest")
|
||||
extracted = root / rak_chain.PHYSICALLY_PASSED_FD98_ROOT_NAME
|
||||
args = argparse.Namespace(bundle=archive)
|
||||
with (
|
||||
mock.patch.object(
|
||||
rak_chain,
|
||||
"sha256_file_limited",
|
||||
return_value=rak_chain.PHYSICALLY_PASSED_FD98_ASSET_SHA256,
|
||||
),
|
||||
mock.patch.object(
|
||||
rak_chain, "extract_bundle", return_value=extracted
|
||||
) as extract_bundle,
|
||||
):
|
||||
self.assertEqual(rak_chain.locate_bundle(args, root / "work"), extracted)
|
||||
extract_bundle.assert_called_once_with(
|
||||
archive.resolve(),
|
||||
root / "work" / "bundle",
|
||||
rak_chain.PHYSICALLY_PASSED_FD98_ASSET_SHA256,
|
||||
)
|
||||
|
||||
def test_compact_9_step_release_rejects_changed_anchor(self) -> None:
|
||||
steps = [mock.Mock(target_sha256="") for _ in range(9)]
|
||||
for number, image_sha256 in rak_chain.COMPACT_RELEASE_ANCHORS:
|
||||
|
||||
Reference in new issue
Block a user