# matroskin architecture
This reference describes the current single-package layout, actor ownership model, wire protocol, and test boundaries. Read [refactor.md](refactor.md) for the decision ledger and [the API documents](api-doc/) for command-specific wire contracts.
## Repository layout
```text
matroskin/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── account.rs # API accounts
│ ├── auth_data.rs # token generation and AES-256-ECB
│ ├── command.rs # Command trait and command registry
│ ├── error.rs # public Error and Result types
│ ├── password.rs # built-in and custom credentials
│ ├── permission.rs # device permission parsing and checks
│ ├── redacted.rs # zeroizing, formatted-secret wrapper
│ ├── request.rs # wire request envelope
│ ├── response.rs # response envelope and typed decoding
│ ├── actor/
│ │ ├── mod.rs # public handle and actor_loop
│ │ ├── connect.rs # TCP connection and handshake
│ │ ├── codec.rs # length-prefixed framing codec
│ │ └── state.rs # private FSM and public status projection
│ └── command/ # 39 command implementations
├── tests/
│ ├── mock_device.rs # loopback integration-test entry point
│ ├── mock_device/ # test-only mock miner and scenarios
│ └── real_device.rs # feature-gated hardware tests
└── docs/
├── architecture.md
├── E2E.md
├── MOCK.md
├── refactor.md
└── api-doc/ # WhatsMiner v3 protocol source material
```
`get.log.download` and `set.system.update_firmware` remain crate-private protocol operations. Callers use `Actor::download_log` and `Actor::upload_firmware` instead.
## Actor ownership
`Actor` is a public handle plus flat connection configuration. Constructing it spawns one Tokio task and starts the actor in `Created`; construction performs no network I/O. The actor task exclusively owns the mutable finite state machine, TCP transport, session salt, and permissions. Production code does not use a mutex around these resources.
```mermaid
stateDiagram-v2
[*] --> Created: Actor::new
Created --> Connecting: connect intent
Closed --> Connecting: connect intent
Connecting --> Connected: TCP and handshake succeed
Connecting --> Reconnecting: failure with retry policy
Connecting --> Closed: failure without retry
Connected --> Downloading: download mailbox
Downloading --> Connected: exact archive transfer succeeds
Connected --> Uploading: upload mailbox
Uploading --> Connected: structured device error
Uploading --> Reconnecting: successful confirmation and device reboot
Connected --> Reconnecting: I/O, timeout, or protocol failure
Downloading --> Reconnecting: transfer failure
Uploading --> Reconnecting: transfer failure
Reconnecting --> Connecting: retry deadline
Reconnecting --> Closed: retry exhausted or shutdown
Connected --> Disconnecting: shutdown intent
Disconnecting --> Closed: session dropped and queues closed
Created --> Closed: shutdown intent
```
The internal `ActorState` is not public. Consumers observe the current `ActorStatus` through `status()` or `watch_status()`. A status projection includes permissions only while a valid session exists; it never includes the transport or salt.
`Actor::clone()` copies only connection configuration and spawns a separate task with fresh channels in `Created`. Use `Arc<Actor>` when several tasks must share one running actor.
## Mailboxes and lifecycle
The handle exposes three independent bounded waiting mailboxes:
- `mpsc(1)` for ordinary owned JSON commands
- `mpsc(1)` for an `AsyncWrite` log destination
- `mpsc(1)` for an `AsyncRead` firmware source and declared length
Lifecycle intents use a separate capacity-4 mailbox. `connect()` and `shutdown()` are non-blocking `try_send` operations. The actor reduces pending lifecycle intents through `PendingPlan` before applying them.
A private generation counter invalidates queued and capacity-blocked operations when the actor closes a session. Those operations receive `Error::ActorClosed`. Reconnect preserves waiting operations and does not advance the generation. Calling `connect()` from `Closed` reopens the same handle; it does not create new mailboxes.
During `Connected`, the actor selects between lifecycle intents, all three operation queues, incoming frames, and the idle heartbeat deadline. Streaming owns the transport exclusively. Lifecycle intents received during an active stream are consumed and ignored because the stream has already claimed the transport boundary.
## Command flow and session safety
An ordinary command follows this sequence:
1. The caller passes an owned command to `actor.send(command)`.
2. The handle reserves mailbox capacity without checking public status first.
3. The actor waits for `Connected(Session)` and checks `PermissionLevel` before encoding.
4. Secured commands receive authentication data. Encrypted commands serialize parameters and encrypt them with AES-256-ECB and PKCS7 padding.
5. The actor sends one framed request and reads the matching response under `IO_TIMEOUT`.
6. Unsolicited `get.miner.report` frames are drained while waiting for the expected response.
7. The caller parses the response outside the actor loop and returns a private validation acknowledgement.
8. A successful typed decode or complete device error preserves the session. Invalid UTF-8, malformed envelopes, typed-payload failures, timeouts, transport errors, and cancelled validation drop the session and trigger reconnect or closure.
`Session` owns `Framed<TcpStream, WhatsMinerCodec>`, a non-copyable `Salt`, and `Permission`. The salt is zeroized when the session drops and never appears in `ActorStatus`, `Debug`, or `Display` output. Credentials and authentication material use `Redacted` and zeroization where applicable.
## Direct streaming
`download_log(destination)` sends the initial framed request, reads the declared `logsize`, switches the socket to raw mode, writes exactly that many archive bytes to the supplied `AsyncWrite`, and restores framed mode. A short transfer, timeout, write failure, or framing failure loses the session.
`upload_firmware(source, len)` performs the authenticated readiness exchange, sends a four-byte little-endian length, and reads exactly `len` bytes from the supplied `AsyncRead`. It then reads the framed confirmation. A successful confirmation models the device reboot and discards the session. A complete device-error confirmation preserves it; a short source, timeout, protocol error, or transport failure loses it.
Both methods use owned `AsyncRead` or `AsyncWrite` values and return `Result<()>`. They do not implement `Command` and expose no progress handle.
## Framing and timing
`WhatsMinerCodec` frames opaque bytes and heartbeats only. JSON and UTF-8 parsing belong to response consumers. Raw binary streaming belongs to the actor's transport code.
| Data | four-byte little-endian length followed by payload | JSON or protocol payload |
| Heartbeat | `00 00 00 00` | zero-length keep-alive frame |
The codec returns data as `bytes::Bytes` using `BytesMut::split_to(...).freeze()`. It grows incrementally and has no artificial 8192-byte payload limit. The actor sends a heartbeat after 60 seconds without I/O activity. Every individual network operation uses the unified `IO_TIMEOUT` of 300 ms. That timeout is an identified hardware risk because physical controller load and network latency have not been exercised in CI.
## Authentication and permissions
For secured operations, the token digest input is:
```text
command name + password + session salt + Unix timestamp
```
SHA-256 supplies the token material and the AES-256 key. Only commands whose API contract marks parameters as sensitive use encrypted parameters. The current encrypted commands are `set.miner.pools` and `set.user.change_passwd`.
The handshake requests `get.device.info` twice: once for the per-connection salt and once for the account's `miner.permission` entry. Permission checks happen before command execution and are distinct from wire security levels.
## Testing boundaries
The default unit and integration tests use the in-process mock miner in `tests/mock_device/`. The mock tests framing, mailbox waiting, reconnect behavior, session-loss rules, unsolicited reports, and raw streaming without requiring hardware. It deliberately does not validate production authentication or decrypt parameters.
The hardware suite in `tests/real_device/` compiles only with `--features real-device`. It uses `MINER_ADDR` and can change device state, reboot the controller, upload firmware, or alter credentials. Physical hardware execution is not part of automated CI.
See [MOCK.md](MOCK.md) for mock-server conventions and [E2E.md](E2E.md) for hardware-test safety. Run repository checks through the aliases in `.cargo/config.toml`, including `cargo test-mock`, `cargo lint`, `cargo fmt-check`, `cargo doc-check`, and `cargo test-real-list`.