Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions doc/mintlayer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# Mintlayer application: Technical Specifications

This page details the protocol implemented for the Mintlayer Ledger app.

## Framework

### APDUs

The messaging format of the app is compatible with the standard [APDU protocol](https://developers.ledger.com/docs/device-app/explanation/io#apdu-interpretation-loop).

The class byte used for all standard commands is `CLA = 0xE1`.

| CLA | INS | COMMAND NAME | DESCRIPTION |
| --- | --- | -------------- | ------------------------------------------------------------------ |
| E1 | 00 | GET_PUBLIC_KEY | Return (and optionally show on screen) a public key and chain code |
| E1 | 01 | SIGN_TX | Sign a transaction through a multi-step process |
| E1 | 02 | SIGN_MSG | Sign a generic message |

### Chunking and P2

The APDU buffer on the Ledger device has a maximum data length (`MAX_ADPU_DATA_LEN` = 255 bytes). To accommodate larger payloads, the app implements a chunking mechanism controlled by the `P2` parameter:

- `P2 = 0x00` (`P2_DONE`): This is the final chunk (or the only chunk) of the instruction. The app will assemble the buffer and execute the command.
- `P2 = 0x80` (`P2_MORE`): More chunks follow. The app accumulates the data into a buffer (up to a maximum of `1020` bytes) and returns `0x9000` (Success) to ask the client for the next chunk.

_Note: For chunked commands, `INS` and `P1` must remain identical across all chunks of the same sequence._

### Data Serialization

The app uses **SCALE codec** (`parity_scale_codec`) for the serialization and deserialization of the data payloads. Vector length prefixes and standard variable-length integer types are represented using SCALE's Compact encoding.

## Status Words

The application returns standard Ledger status words as well as app-specific and ECC cryptographic errors.

| SW | SW name | Description |
| ------ | ------------------------------ | ---------------------------------------------- |
| 0x9000 | `Ok` | Success |
| 0x6700 | `WrongApduLength` | Incorrect APDU length |
| 0x6985 | `Deny` | Rejected by user / User cancelled |
| 0x6B00 | `WrongP1P2` | Either `P1` or `P2` is incorrect |
| 0x6D00 | `InsNotSupported` | No command exists with `INS` |
| 0x6E00 | `ClaNotSupported` | Bad `CLA` used for this application |
| 0xB000 | `TxDisplayFail` | Transaction display failed |
| 0xB001 | `AddrDisplayFail` | Address display failed |
| 0xB002 | `TxWrongLength` | Transaction wrong length |
| 0xB003 | `TxParsingFail` | Transaction parsing failed |
| 0xB004 | `TxHashFail` | Transaction hashing failed |
| 0xB005 | `TxAddressFail` | Transaction address failed |
| 0xB006 | `TxSignFail` | Transaction signing failed |
| 0xB007 | `KeyDeriveFail` | Key derivation failed |
| 0xB008 | `VersionParsingFail` | Version parsing failed |
| 0xB009 | `WrongContext` | Wrong context (e.g., Next called before Start) |
| 0xB00A | `DeserializeFail` | SCALE deserialization failed |
| 0xB00B | `TxInvalidInputUtxo` | Invalid input UTXO |
| 0xB00C | `TxNumericOperationFail` | Numeric operation failed |
| 0xB00D | `TxUnsupportedInput` | Unsupported input |
| 0xB00E | `TxInvalidTokenV0` | Invalid Token V0 |
| 0xB00F | `TxInvalidInputPath` | Invalid input path |
| 0xB010 | `NothingToSign` | Nothing to sign |
| 0xB011 | `TxAlreadyFinished` | Transaction already finished |
| 0xB012 | `InvalidPath` | Invalid BIP32 path |
| 0xB013 | `InvalidUncompressedPublicKey` | Invalid uncompressed public key |
| 0xB014 | `MaxBufferLenExceeded` | Max buffer length exceeded (Chunking limit) |
| 0xB015 | `DifferentInputCommitmentHash` | Different input commitment hash |
| 0xB016 | `OrdersV0NotSupported` | Orders V0 not supported |
| 0xB100 | `EccCarry` | ECC Carry |
| 0xB101 | `EccLocked` | ECC Locked |
| 0xB102 | `EccUnlocked` | ECC Unlocked |
| 0xB103 | `EccNotLocked` | ECC Not Locked |
| 0xB104 | `EccNotUnlocked` | ECC Not Unlocked |
| 0xB105 | `EccInternalError` | ECC Internal Error |
| 0xB106 | `EccInvalidParameterSize` | ECC Invalid Parameter Size |
| 0xB107 | `EccInvalidParameterValue` | ECC Invalid Parameter Value |
| 0xB108 | `EccInvalidParameter` | ECC Invalid Parameter |
| 0xB109 | `EccNotInvertible` | ECC Not Invertible |
| 0xB10A | `EccOverflow` | ECC Overflow |
| 0xB10B | `EccMemoryFull` | ECC Memory Full |
| 0xB10C | `EccNoResidue` | ECC No Residue |
| 0xB10D | `EccPointAtInfinity` | ECC Point At Infinity |
| 0xB10E | `EccInvalidPoint` | ECC Invalid Point |
| 0xB10F | `EccInvalidCurve` | ECC Invalid Curve |
| 0xB110 | `EccGenericError` | ECC Generic Error |

---

## Commands

### GET_PUBLIC_KEY

Returns a public key and chain code at the given derivation path.
Optionally displays the generated address on the device screen for user verification.

#### Encoding

**Command**

| _CLA_ | _INS_ | _P1_ |
| ----- | ----- | -------------------- |
| E1 | 00 | `0` or `1` (Display) |

**Input data (`PublicKeyReq` - SCALE encoded)**

| Type | Name | Description |
| ----------- | ----------- | --------------------------------------------------------- |
| `u8` (Enum) | `coin_type` | `0` = Mainnet, `1` = Testnet, `2` = Regtest, `3` = Signet |
| `Vec<u32>` | `path` | The BIP32 derivation path |

**Output data (`GetPublicKeyRespones` - SCALE encoded)**

| Length | Description |
| ------ | --------------------------- |
| `65` | The uncompressed public key |
| `32` | The chain code |

#### Description

If `P1` is `0`, the application derives the public key and returns it silently.
If `P1` is `1`, the application will display the address derived from the requested path on the device screen.
The command will only return `Ok` (0x9000) if the user approves it, otherwise, it returns `Deny` (0x6985).

---

### SIGN_TX

Signs a Mintlayer transaction.
Because transactions can be larger than available APDU buffers and RAM, the parsing and signing process is split into an interactive state machine using `P1 = 0` (Start) and `P1 = 1` (Next).

#### Encoding

**Command**

| _CLA_ | _INS_ | _P1_ |
| ----- | ----- | ------------------------- |
| E1 | 01 | `0` (Start) or `1` (Next) |

**Input Data for `P1 = 0` (Start) (`TxMetadataReq` - SCALE encoded)**

| Type | Name | Description |
| ----- | ------------- | --------------------------------------------------------- |
| `u8` | `coin` | `0` = Mainnet, `1` = Testnet, `2` = Regtest, `3` = Signet |
| `u8` | `version` | Transaction version |
| `u32` | `num_inputs` | Total number of inputs in the transaction |
| `u32` | `num_outputs` | Total number of outputs in the transaction |

**Input Data for `P1 = 1` (Next) (`SignTxReq` Enum - SCALE encoded)**

The client sends a sequence of `SignTxReq` variants. The variant index dictates the type of data being sent:

- `Input` (Index 0): Contains `TxInputReq` (Input address paths and UTXO/Account info).
- `InputCommitment` (Index 1): Contains `SighashInputCommitment`.
- `Output` (Index 2): Contains `TxOutputReq`.
- `NextSignature` (Index 3): Requests the device to yield the next available signature.

**Output data (`SignatureResponse` - SCALE encoded)**

Yielded during `NextSignature` sequences.

| Type | Name | Description |
| ------------- | -------------- | ------------------------------------------- |
| `[u8; 64]` | `signature` | The 64-byte cryptographic signature |
| `Option<u32>` | `multisig_idx` | Optional multisig index |
| `u32` | `input_idx` | The index of the input that was just signed |
| `bool` | `has_next` | True if there are more signatures remaining |

#### Description

To sign a transaction, the client must follow a strict order:

1. Call `SIGN_TX` with `P1 = 0` (Start) passing the overall transaction metadata (`TxMetadataReq`).
2. Sequentially call `SIGN_TX` with `P1 = 1` (Next) to stream inputs `Input`, input commitments `InputCommitment` and then outputs (`Output`).
3. After all data is verified by the user on the device's secure screen, the client requests signatures by repeatedly calling `SIGN_TX` with `P1 = 1` and the `NextSignature` variant.
4. The device will yield `SignatureResponse` payloads until `has_next` is false.

---

### SIGN_MSG

Signs a generic message using a BIP-32 derived key. The process is stateful to allow streaming long messages.

#### Encoding

**Command**

| _CLA_ | _INS_ | _P1_ |
| ----- | ----- | ------------------------- |
| E1 | 02 | `0` (Start) or `1` (Next) |

**Input Data for `P1 = 0` (Start) (`SignMessageReq` - SCALE encoded)**

| Type | Name | Description |
| ----------- | ----------- | --------------------------------------------- |
| `u8` (Enum) | `coin` | Coin type (Mainnet, Testnet, Regtest, Signet) |
| `u8` (Enum) | `addr_type` | `0` = PublicKey, `1` = PublicKeyHash |
| `Vec<u32>` | `path` | The BIP32 derivation path to use for signing |

**Input Data for `P1 = 1` (Next)**

| Type | Description |
| ------------ | ------------------------------------------------------------- |
| `<variable>` | The raw byte chunks of the message payload to be accumulated. |

**Output data (`MsgSignature` - SCALE encoded)**

| Length | Description |
| ------ | ----------------------------------- |
| `64` | The 64-byte cryptographic signature |

#### Description

The `SIGN_MSG` flow initializes the signing context via `Start`.
The client then sends the message chunks iteratively using `Next`.
Once the message transmission is completed and the user validates the request on the hardware screen,
the device returns the 64-byte signature.
Chunking the payload via the `P2` parameter mechanism is utilized here if the data exceeds a single APDU packet limit.