flowrs-engine/crates/flowrs-transport/PROVENANCE.md
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

11 KiB
Raw Permalink Blame History

Provenance of flowrs-transport

Everything in this crate is written for flowRS and licensed MIT OR Apache-2.0. Per-file origins are listed in tools/runtime-licence.tsv, which tools/runtime-licence-audit.sh checks. This file records the clean-room rewrite below in more detail than one register row can.

The ESP8266 ROM erase size: src/esptool/esp8266_rom.rs (clean room, 2026-10-27)

Why it was rewritten

The ESP8266 mask-ROM loader erases more flash than FLASH_BEGIN asks for, so a host must hand it a different number from the image size. flowRS used to carry that rule as crates/flowrs-boards/src/esp8266_rom.rs. That file was a transcription of ESP8266ROM.get_erase_size in esptool.py, which is GPL-2.0-or-later. flowRS's porting policy lets a GPL tool be used as an oracle, never as a source. Commit e4a336ba removed the file. The module here replaces it. Nobody who had read the GPL code or its transcription wrote it.

How it was written

The implementer was an AI agent (Claude Opus 5.5) working on branch work/cleanroom. It started from a specification written by a coordinator who had read the old file. The specification held no code: the function's signature and purpose, and the seven assertions of the old tests. Under the clean-room rules, the implementer did not open the old file, its git history, esptool.py's source, or esptool's documentation. It used only the sources below.

Source Licence What was taken
The coordinator's specification — Signature erase_size(offset: u32, size: u32) -> u32, the purpose, and seven pinned values
ESP8266_RTOS_SDK, commit 858c7c2e: components/spi_flash/include/spi_flash.h Apache-2.0 SPI_FLASH_SEC_SIZE is 4096
ESP8266_RTOS_SDK: components/esp8266/include/esp8266/rom_functions.h, components/esp8266/ld/esp8266.rom.ld, components/bootloader/subproject/main/esp8266.bootloader.rom.ld Apache-2.0 The ROM's flash descriptor carries block_size and sector_size; the ROM exports SPIEraseSector
ESP8266_RTOS_SDK: components/bootloader_support/src/bootloader_init.c Apache-2.0 The ROM flash descriptor is configured with 64 KiB blocks and 4 KiB sectors (in the file's ESP32 branch; it confirms the descriptor's convention, not an ESP8266 value)
ESP8266_RTOS_SDK: components/spi_flash/src/spi_flash.c, spi_flash_raw.c, include/esp_spi_flash.h, components/esp8266/source/chip_boot.c Apache-2.0 Searched for erase and size constants; nothing beyond the above
ESP8266_NONOS_SDK, commit 8e138654: ld/eagle.rom.addr.v6.ld, include/spi_flash.h ESPRESSIF MIT License (MIT terms, limited to use on Espressif's ESP8266) The ROM has SPIEraseArea, SPIEraseBlock, SPIEraseSector and SPIEraseChip; its SpiFlashChip carries block_size and sector_size. Facts only; no code is in flowRS
esptool.py 5.6.0 and 4.8.1 from PyPI, installed in throwaway virtual environments on the build host GPL-2.0-or-later Run as a black-box oracle through tools/esp8266_erase_oracle.py: inputs and outputs only, with tracebacks off so no source line could print. The method's interface, (self, offset, size), was read with inspect.signature
src/esptool.rs in this crate (the version after commit b1b3f743) MIT OR Apache-2.0 ESP32-family ROMs and the flasher stub take the plain image size; FLASH_SECTOR_SIZE

Espressif does not document the miscount. One web search (ESP8266 ROM SPIEraseArea sector block erase espressif, with docs.espressif.com excluded) returned a summary that paraphrased an esptool issue and a Cesanta blog post: an erase that crosses a sixteen-sector block boundary erases extra head sectors. The implementer did not open those pages. The model below was fitted to the oracle's outputs, not to that summary.

The rule, derived from the oracle

The implementer tabulated the oracle's output for every start sector within a block (and a few in later blocks) against image sizes of 0 to 40 sectors. It also probed byte-granular sizes and unaligned offsets. The table fits one model exactly.

Count in 4 KiB sectors. Let start be the sector that holds the offset, head the sectors from start to the end of its 64 KiB block (1 to 16), and handed the sectors FLASH_BEGIN asks for. The ROM then erases min(2 × handed, handed + head) sectors. To cover an image of n sectors, hand it the least handed for which that reaches n, which is max(⌈n / 2⌉, n − head). A partial sector counts as a whole one, and an unaligned offset counts as the start of its sector. The outputs repeat every sixteen sectors, which confirms the 64 KiB block.

Verification

  • Sweep: 52,348,517 inputs, 0 disagreements. Each of esptool.py 5.6.0 and 4.8.1 was piped into examples/esp8266_erase_check.rs, which calls flowRS's rule. The inputs were:
    • every 4 KiB-aligned offset from 0 to 16 MiB (4,097 offsets) against size 0 and every size one below, on, and one above each 4 KiB multiple up to 16 MiB (12,289 sizes);
    • 22 × 22 corner values up to 2^32 − 1;
    • one million seeded random inputs anywhere in the 32-bit space;
    • one million seeded random inputs inside 16 MiB.
  • Pinned vectors: tests/data/esp8266_erase_oracle.tsv holds 396 oracle vectors (18 offsets by 22 sizes), generated by tools/esp8266_erase_oracle.py fixture. The test tests/esp8266_rom_oracle.rs checks every one.
  • Unit tests in the module: the seven specified values. They also prove that the result is the least whole-sector count whose modelled erase covers the image, that the ROM over-erases by one sector only for an odd image no longer than twice head, and that the largest inputs do not overflow.
  • Mutation check: tools/mutate-esp8266-rom.sh.

What the implementer was exposed to, stated plainly

  • The old file, through a search mistake. While looking for the rule's callers, the implementer ran a recursive grep and tried to filter the old file out of the results. The filter expected ./-prefixed paths, which this shell's grep does not print, so it failed. As a result, 15 matching lines of the old file were shown, as follows:

    • three lines of its provenance header. They said the file was GPL-derived, and that the flasher stub's erase size is the plain size (a fact src/esptool.rs also states);
    • the first line of the function's doc comment, and its signature (already in the specification);
    • the test's name, and a test comment naming a sector-size constant of 0x1000;
    • the seven test assertions (already in the specification);
    • one comment line inside the function body. Cut off mid-sentence, it said the number sixteen is a literal in esptool.py's code.

    No other line of the function body was shown. Every fact in those lines is in the specification, in src/esptool.rs, or in Espressif's SDKs (4 KiB sectors, 64 KiB blocks, so sixteen sectors to a block). The implementer then deleted the file without opening it and excluded it from every later search.

  • Training data. The implementer is a language model. Its training data may have included esptool.py's public source, and it cannot certify otherwise. In this session the rule came from the oracle's outputs and is expressed differently: a model of the ROM (rom_erased_sectors) and the closed-form least cover of that model, max(⌈n / 2⌉, n − head), which the tests check against the model. esptool.py was run, not read.

Re-cited to the primary specifications: src/avr.rs and src/dfu.rs (2026-10-27)

The licence audit found that these files cited GPL tools as the place their protocol facts were read: avrdude (GPL-2.0-or-later) for the AVR bootloader protocols, and WebDFU (ISC, itself ported from dfu-util, GPL-2.0-or-later) for DFU. Facts are not copyrightable, but the porting policy wants them cited to the specifications that publish them. Each constant and layout was checked against the documents below only, never against the GPL tools. Every value matched. Each builder emits one byte sequence per command, in the order the specification's command table gives. That table dictates the structure; nothing in it follows a GPL tool's code. The comments now cite the specifications.

Source Licence Checked in this crate
Atmel AVR061, STK500 Communication Protocol (2525B, 04/03), and the command.h Atmel published with it (AVR061.zip, 2002) Atmel/Microchip documentation; facts only avr::stk500v1: Sync_CRC_EOP 0x20; Resp_STK_OK 0x10, FAILED 0x11, INSYNC 0x14, NOSYNC 0x15; commands 0x30, 0x41, 0x50, 0x51, 0x52, 0x55, 0x64, 0x74, 0x75; Parm_STK_SW_MAJOR 0x81. Also the address low byte first, the page size high byte first, and the F memory type
Atmel AVR068, STK500 Communication Protocol (2591C, 06/06), and its command.h (AVR068.zip, 2005) Atmel/Microchip documentation; facts only avr::stk500v2. Table 3-1 gives the frame: MESSAGE_START 0x1B, sequence, size MSB first, TOKEN 0x0E, body, and an XOR checksum over every byte. The commands are 0x01, 0x06, 0x10 to 0x14 and 0x1B; STATUS_CMD_OK is 0x00 and STATUS_CMD_FAILED 0xC0; the answer body is [answer id, status, …]
Atmel AVR109, Self Programming (1644G, 06/04), Table 2, and its reference bootloader main.c (AVR109.zip) Atmel/Microchip documentation and sample code; facts only avr::avr109: the command characters and the carriage-return acknowledgement. main.c shows addresses and block sizes read high byte first, and flash addresses counted in words
USB-IF, Device Class Specification for Device Firmware Upgrade, revision 1.1 (2004) USB-IF specification; facts only dfu: bmRequestType 0x21/0xA1 (Table 3.1) and request codes 0 to 6 (Table 3.2). The block number goes in wValue for DNLOAD (section 6.1.1) and UPLOAD (section 6.2.1). Section 6.1.2 gives the 6-byte GETSTATUS reply with a 3-byte bwPollTimeout, the status codes 0x00 to 0x0F and the states 0 to 10
ST AN3156, USB DFU protocol used in the STM32 bootloader ST documentation Cited as DfuSe's specification. Not read in this session: st.com did not answer requests from the build network, and no mirror was reachable. Someone should confirm the DfuSe values against AN3156 itself
ST's STM32 USB device library, DFU class (STMicroelectronics/stm32-mw-usb-device, Class/DFU/Inc/usbd_dfu.h and Class/DFU/Src/usbd_dfu.c) SLA0044; facts only, no code in flowRS dfu DfuSe: the commands ride on DNLOAD with wBlockNum 0 (GET_COMMANDS 0x00, SET_ADDRESS_POINTER 0x21, ERASE 0x41) and carry a 32-bit little-endian address. Data blocks start at 2, at (wBlockNum − 2) × transfer size past the address pointer. The DFU status and state values agree with DFU 1.1

The programmer ids in avr.rs's table (arduino, urclock, wiring, avr109, …) are the names boards use in Arduino's upload.protocol, which are avrdude's programmer ids. They are named for interoperability; nothing else from avrdude is used.