From d8bbe073d97e99279237b29f785629bda7c5374b Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Tue, 29 Jul 2025 11:12:57 +0200 Subject: [PATCH 01/38] account examples, no btype for legacy blocks --- standards/ICRC-3/README.md | 78 ++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index c06f053..c82ebd0 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -103,7 +103,6 @@ digit = "0" / nonzero_digit op_name = a-z *(a-z / digit / "_" / "-") ``` -For instance, `1xfer` is the identifier of the ICRC-1 transfer operation. ## Supported Standards @@ -111,14 +110,43 @@ An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported bl ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema -ICRC-1 and ICRC-2 use the `tx` field to store input from the user and use the external block to store data set by the Ledger. For instance, the amount of a transaction is stored in the field `tx.amt` because it has been specified by the user, while the time when the block was added to the Ledger is stored in the field `ts` because it is set by the Ledger. +This section describes how ICRC-1 and ICRC-2 operations are recorded in ICRC-3-compliant blocks. + + + +### No `btype` Field +ICRC-1 and ICRC-2 blocks MUST NOT include a `btype` field. These standards use the legacy block format where the type of block is determined exclusively by the content of the `tx` field. ICRC-1 and ICRC-2 blocks use the `tx` field to store input from the user and use the external block to store data set by the Ledger. For instance, the amount of a transaction is stored in the field `tx.amt` because it has been specified by the user, while the time when the block was added to the Ledger is stored in the field `ts` because it is set by the Ledger. + +#### Block Structure Requirements A generic ICRC-1 or ICRC-2 Block: +- **MUST** use the legacy format, i.e., it **MUST NOT** include a `"btype"` field. +- **MUST** be a `Value::Map` containing at least the following fields: + - `"phash"`: `Blob` — the hash of the parent block. + - `"ts"`: `Nat` — the timestamp (set by the ledger at block creation). + - `"tx"`: `Value::Map` — representing the user’s transaction intent. +- **CAN** include: + - `"fee"`: `Nat` — only if the ledger requires a fee and the user did not specify one in `tx.fee`. + + +#### `tx` Field Semantics + +The `tx` field: + +- **MUST** represent the user intent derived from the method call. +- **MUST** be encoded using the ICRC-3 `Value` type. +- **MUST NOT** contain any fields that were not explicitly present in the original user call. +- **MUST** follow the canonical mapping rules described in the next section. + + + + + 1. it MUST contain a field `ts: Nat` which is the timestamp of when the block was added to the Ledger 2. if the operation requires a fee and if the `tx` field doesn't specify the fee then it MUST contain a field `fee: Nat` which specifies the fee payed to add this block to the Ledger 3. its field `tx` - 1. CAN contain a field `op: String` that uniquely defines the type of operation + 1. MUST contain a field `op: String` that uniquely defines the type of operation 2. MUST contain a field `amt: Nat` that represents the amount 3. MUST contain the `fee: Nat` field for operations that require a fee if the user specifies the fee in the request. If the user does not specify the fee in the request, then this field is not set and the top-level `fee` is set. 4. CAN contain the `memo: Blob` field if specified by the user @@ -126,40 +154,40 @@ A generic ICRC-1 or ICRC-2 Block: Operations that require paying a fee: Transfer, and Approve. -The type of a generic ICRC-1 or ICRC-2 Block is defined by either the field `btype` or the field `tx.op`. The first approach is preferred, the second one exists for backward compatibility. If both are specified then `btype` defines the type of the block regardless of `tx.op`. `icrc3_supported_block_types` should always return all the `btype`s supported by the Ledger even if the Ledger doesn't support the `btype` field yet. For example, if the Ledger supports mint blocks using the backward compatibility schema, i.e. without `btype`, then the endpoint `icrc3_supported_block_types` will have to return `"1mint"` among the supported block types. ### Account Type -ICRC-1 Account is represented as an `Array` containing the `owner` bytes and optionally the subaccount bytes. +ICRC-1 Account is represented as an `Array` containing the `owner` bytes and optionally the subaccount bytes. Two examples of accounts, one with subaccount and the second without are below. -### Burn Block Schema +Example of account representation as an array with two blobs, one for the owner principal and the second for the subaccount: -1. the `btype` field MUST be `"1burn"` or `tx.op` field MUST be `"burn"` -2. it MUST contain a field `tx.from: Account` +``` +variant { Array = vec { + variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; + variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; + }}; +``` -Example with `btype`: + +Example of account representation as an array with one blob encoding the owner principal. ``` -variant { Map = vec { - record { "btype"; "variant" { Text = "1burn" }}; - record { "phash"; variant { - Blob = blob "\a1\a9p\f5\17\e5\e2\92\87\96(\c8\f1\88iM\0d(tN\f4-~u\19\88\83\d8_\b2\01\ec" - }}; - record { "ts"; variant { Nat = 1_701_108_969_851_098_255 : nat }}; - record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat = 1_228_990 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\020\00\07\01\01" }; - variant { Blob = blob "&\99\c0H\7f\a4\a5Q\af\c7\f4;\d9\e9\ca\e5 \e3\94\84\b5c\b6\97/\00\e6\a0\e9\d3p\1a" }; - }}}; - record { "memo"; variant { Blob = blob "\82\00\83x\223K7Bg3LUkiXZ5hatPT1b9h3XxJ89DYSU2e\19\07\d0\00" +variant { Array = vec { + variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; + }}; - }}}; -}}; ``` -Example without `btype`: + +### Burn Block Schema + +1. the `btype` MUST not be set and `tx.op` field MUST be `"burn"` +2. it MUST contain a field `tx.from: Account` +3. it MUSAT contain a field `tx.amt: Nat` +4. it MUST contain a field `tx.memo` if the `icrc1_transfer` call that creates the block has a memo field, and its value is the value of that field. + + ``` variant { Map = vec { record { "phash"; variant { From 6e85916450c47e390d0d1d5228c024a1637f7bb9 Mon Sep 17 00:00:00 2001 From: bogwar <51327868+bogwar@users.noreply.github.com> Date: Wed, 6 Aug 2025 21:00:21 +0200 Subject: [PATCH 02/38] Update standards/ICRC-3/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- standards/ICRC-3/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index c82ebd0..8b94bea 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -182,7 +182,7 @@ variant { Array = vec { ### Burn Block Schema -1. the `btype` MUST not be set and `tx.op` field MUST be `"burn"` +1. the `btype` MUST NOT be set and `tx.op` field MUST be `"burn"` 2. it MUST contain a field `tx.from: Account` 3. it MUSAT contain a field `tx.amt: Nat` 4. it MUST contain a field `tx.memo` if the `icrc1_transfer` call that creates the block has a memo field, and its value is the value of that field. From ce7be73a8d98b438c9ae1db4910b2b212aa9edc6 Mon Sep 17 00:00:00 2001 From: bogwar <51327868+bogwar@users.noreply.github.com> Date: Wed, 6 Aug 2025 21:00:32 +0200 Subject: [PATCH 03/38] Update standards/ICRC-3/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- standards/ICRC-3/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 8b94bea..b39fcf8 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -184,7 +184,7 @@ variant { Array = vec { 1. the `btype` MUST NOT be set and `tx.op` field MUST be `"burn"` 2. it MUST contain a field `tx.from: Account` -3. it MUSAT contain a field `tx.amt: Nat` +3. it MUST contain a field `tx.amt: Nat` 4. it MUST contain a field `tx.memo` if the `icrc1_transfer` call that creates the block has a memo field, and its value is the value of that field. From c0201b0bf0c992b9a64a2559839cdbfb7bd2cba6 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 15 Aug 2025 14:38:05 +0200 Subject: [PATCH 04/38] added principles of recording transactions in blocks and extended Interaction with other standards --- standards/ICRC-3/README.md | 445 ++++++++++++++++++++++++------------- 1 file changed, 295 insertions(+), 150 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index c82ebd0..3e39bc0 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -117,7 +117,7 @@ This section describes how ICRC-1 and ICRC-2 operations are recorded in ICRC-3-c ### No `btype` Field ICRC-1 and ICRC-2 blocks MUST NOT include a `btype` field. These standards use the legacy block format where the type of block is determined exclusively by the content of the `tx` field. ICRC-1 and ICRC-2 blocks use the `tx` field to store input from the user and use the external block to store data set by the Ledger. For instance, the amount of a transaction is stored in the field `tx.amt` because it has been specified by the user, while the time when the block was added to the Ledger is stored in the field `ts` because it is set by the Ledger. -#### Block Structure Requirements +#### Block Structure A generic ICRC-1 or ICRC-2 Block: @@ -138,24 +138,25 @@ The `tx` field: - **MUST** be encoded using the ICRC-3 `Value` type. - **MUST NOT** contain any fields that were not explicitly present in the original user call. - **MUST** follow the canonical mapping rules described in the next section. +- **MUST** contain a field `op: String` with value one of "mint", "burn", "xfer", "approve" +- **MUST** contain a field `amt: Nat` that represents the amount +- **MUST** contain the `fee: Nat` field for operations that require a fee if the user specifies the fee in the request. If the user does not specify the fee in the request, then this field is not set and the top-level `fee` is set. + - **CAN** contain the `memo: Blob` field if specified by the user + - **CAN** contain the `ts: Nat` field if the user sets the `created_at_time` field in the request. +Operations that require paying a fee: Transfer, and Approve. +### Compliance Reporting +Although legacy ICRC-1 and ICRC-2 blocks do not include the `btype` field, ledgers **MUST** still report their supported block types via the `icrc3_supported_block_types` endpoint. By convention, the following identifiers are used to describe the types of these legacy blocks: -1. it MUST contain a field `ts: Nat` which is the timestamp of when the block was added to the Ledger -2. if the operation requires a fee and if the `tx` field doesn't specify the fee then it MUST contain a field `fee: Nat` which specifies the fee payed to add this block to the Ledger -3. its field `tx` - 1. MUST contain a field `op: String` that uniquely defines the type of operation - 2. MUST contain a field `amt: Nat` that represents the amount - 3. MUST contain the `fee: Nat` field for operations that require a fee if the user specifies the fee in the request. If the user does not specify the fee in the request, then this field is not set and the top-level `fee` is set. - 4. CAN contain the `memo: Blob` field if specified by the user - 5. CAN contain the `ts: Nat` field if the user sets the `created_at_time` field in the request. - -Operations that require paying a fee: Transfer, and Approve. - +- `"1burn"` for burn blocks +- `"1mint"` for mint blocks +- `"1xfer"` for `icrc1_transfer` blocks +- `"2xfer"` for `icrc2_transfer_from` blocks +- `"2approve"` for `icrc2_approve` blocks -`icrc3_supported_block_types` should always return all the `btype`s supported by the Ledger even if the Ledger doesn't support the `btype` field yet. For example, if the Ledger supports mint blocks using the backward compatibility schema, i.e. without `btype`, then the endpoint `icrc3_supported_block_types` will have to return `"1mint"` among the supported block types. ### Account Type @@ -180,172 +181,316 @@ variant { Array = vec { ``` -### Burn Block Schema +### Canonical `tx` Mapping -1. the `btype` MUST not be set and `tx.op` field MUST be `"burn"` -2. it MUST contain a field `tx.from: Account` -3. it MUSAT contain a field `tx.amt: Nat` -4. it MUST contain a field `tx.memo` if the `icrc1_transfer` call that creates the block has a memo field, and its value is the value of that field. +Each ICRC-1 or ICRC-2 method call maps deterministically to the `tx` field of the resulting block. Only parameters provided by the user are included — optional fields that are omitted in the call MUST NOT appear in `tx`. +All fields are encoded using the ICRC-3 `Value` type. +--- + +#### `icrc1_transfer` + +**Call parameters:** + +```candid +icrc1_transfer: record { + to: Account; + amount: Nat; + fee: opt Nat; + memo: opt Blob; + from_subaccount: opt blob; + created_at_time: opt Nat; +} ``` -variant { Map = vec { - record { "phash"; variant { - Blob = blob "\a1\a9p\f5\17\e5\e2\92\87\96(\c8\f1\88iM\0d(tN\f4-~u\19\88\83\d8_\b2\01\ec" - }}; - record { "ts"; variant { Nat = 1_701_108_969_851_098_255 : nat }}; - record { "tx"; variant { Map = vec { - record { "op"; variant { Text = "burn" } }; - record { "amt"; variant { Nat = 1_228_990 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\020\00\07\01\01" }; - variant { Blob = blob "&\99\c0H\7f\a4\a5Q\af\c7\f4;\d9\e9\ca\e5 \e3\94\84\b5c\b6\97/\00\e6\a0\e9\d3p\1a" }; - }}}; - record { "memo"; variant { Blob = blob "\82\00\83x\223K7Bg3LUkiXZ5hatPT1b9h3XxJ89DYSU2e\19\07\d0\00" - }}; - }}}; -}}; -``` -#### Mint Block Schema +**Regular Transfer** — when neither the sender nor recipient is the minting account: + +- `op = "xfer"` +- `from = [caller]` if `from_subaccount` is not provided +- `from = [caller, from_subaccount]` if provided +- `to = to` +- `amt = amount` +- `fee = fee` if provided +- `memo = memo` if provided +- `ts = created_at_time` if provided + + + + +**Transfer from the Minting Account (→ Mint)** — when `[caller]` or `[caller, from_subaccount]` equals the minting account: -1. the `btype` field MUST be `"1mint"` or the `tx.op` field MUST be `"mint"` -2. it MUST contain a field `tx.to: Account` +- `op = "mint"` +- `to = to` +- `amt = amount` +- `memo = memo` if provided +- `ts = created_at_time` if provided +> `from` and `fee` MUST NOT be present + +**Transfer to the Minting Account (→ Burn)** — when `to` equals the minting account: + +- `op = "burn"` +- `from = [caller]` if `from_subaccount` is not provided +- `from = [caller, from_subaccount]` if provided +- `amt = amount` +- `memo = memo` if provided +- `ts = created_at_time` if provided +> `to` and `fee` MUST NOT be present + + +### Canonical Examples of `icrc1_transfer` Blocks + +Each of the following examples represents a canonical block resulting from an `icrc1_transfer` call. These examples illustrate different scenarios depending on which optional fields were included in the call. Only parameters explicitly provided by the caller appear in the resulting `tx`. + +--- + +#### Example 1: Transfer with required parameters only +This example shows an `icrc1_transfer` call where the caller only specifies the mandatory fields: `to` and `amount`. No `memo`, `created_at_time`, or explicit `fee` are provided. The block still contains a top-level `fee` field since the ledger applies the default transfer fee. -Example with `btype`: ``` -variant { Map = vec { - record { "btype"; "variant" { Text = "1mint" }}; - record { "ts"; variant { Nat = 1_675_241_149_669_614_928 : nat } }; - record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat = 100_000 : nat } }; - record { "to"; variant { Array = vec { - variant { Blob = blob "Z\d0\ea\e8;\04*\c2CY\8b\delN\ea>]\ff\12^. WGj0\10\e4\02" }; - }}}; - }}}; -}}; +variant { + Map = vec { + record { "fee"; variant { Nat64 = 10_000 : nat64 } }; + record { + "phash"; + variant { + Blob = blob "\b8\0d\29\e5\91\60\4c\d4\60\3a\2a\7c\c5\33\14\21\27\b8\23\e9\a5\24\b7\14\43\24\4b\2d\d5\b0\86\13" + }; + }; + record { "ts"; variant { Nat64 = 1_753_344_727_778_561_060 : nat64 } }; + record { + "tx"; + variant { + Map = vec { + record { "amt"; variant { Nat64 = 85_224_322_205 : nat64 } }; + record { "from"; variant { Array = vec { variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" } } } }; + record { "op"; variant { Text = "xfer" } }; + record { + "to"; + variant { + Array = vec { + variant { Blob = blob "\09\14\61\93\79\7a\6c\ab\86\17\ee\f9\5f\16\40\94\d3\f8\7c\e9\0d\9e\b2\7e\01\40\0c\79\02" }; + } + }; + }; + } + }; + }; + } +}; ``` -Example without `btype`: +--- + +#### Example 2: Mint to user account +This example represents an `icrc1_transfer` call where the `from` account is the minting account. This results in a mint block. The caller specifies `to` and `amount`. No `fee`, `memo`, or `created_at_time` are provided. + ``` -variant { Map = vec { - record { "ts"; variant { Nat = 1_675_241_149_669_614_928 : nat } }; - record { "tx"; variant { Map = vec { - record { "op"; variant { Text = "mint" } }; - record { "amt"; variant { Nat = 100_000 : nat } }; - record { "to"; variant { Array = vec { - variant { Blob = blob "Z\d0\ea\e8;\04*\c2CY\8b\delN\ea>]\ff\12^. WGj0\10\e4\02" }; - }}}; - }}}; -}}; +variant { + Map = vec { + record { + "phash"; + variant { + Blob = blob "\c2\b1\32\6a\5e\09\0e\10\ad\be\f3\4c\ba\fd\bc\90\18\3f\38\a7\3e\73\61\cc\0a\fa\99\89\3d\6b\9e\47" + }; + }; + record { "ts"; variant { Nat64 = 1_753_344_737_123_456_789 : nat64 } }; + record { + "tx"; + variant { + Map = vec { + record { "amt"; variant { Nat64 = 500_000_000 : nat64 } }; + record { + "to"; + variant { + Array = vec { + variant { Blob = blob "\15\28\84\12\af\11\b2\99\31\3a\5b\5a\7c\12\83\11\de\10\23\33\c4\ad\be\66\9f\2e\a1\a3\08" }; + } + }; + }; + record { "op"; variant { Text = "mint" } }; + } + }; + }; + } +}; ``` -#### Transfer and Transfer From Block Schema +--- -1. the `btype` field MUST be - 1. `"2xfer"` for `icrc2_transfer_from` blocks - 2. `"1xfer"` for `icrc1_transfer` blocks -1. if `btype` is not set then `tx.op` field MUST be `"xfer"` -2. it MUST contain a field `tx.from: Account` -3. it MUST contain a field `tx.to: Account` -4. it CAN contain a field `tx.spender: Account` +#### Example 3: Burn from user account +This example represents an `icrc1_transfer` call where the destination `to` is the minting account. This results in a burn block. The caller specifies `from` and `amount`. No `fee`, `memo`, or `created_at_time` are provided. -Example with `btype`: ``` -variant { Map = vec { - record { "btype"; "variant" { Text = "1xfer" }}; - record { "fee"; variant { Nat = 10 : nat } }; - record { "phash"; variant { Blob = - blob "h,,\97\82\ff.\9cx&l\a2e\e7KFVv\d1\89\beJ\c5\c5\ad,h\5c<\ca\ce\be" - }}; - record { "ts"; variant { Nat = 1_701_109_006_692_276_133 : nat } }; - record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat = 609_618 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; - variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; - }}}; - record { "to"; variant { Array = vec { - variant { Blob = blob " \ef\1f\83Zs\0a?\dc\d5y\e7\ccS\9f\0b\14a\ac\9f\fb\f0bf\f3\a9\c7D\02" }; - variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; - }}}; - }}}; -}}; +variant { + Map = vec { + record { + "phash"; + variant { + Blob = blob "\7f\89\42\a5\be\4d\af\50\3b\6e\2a\8e\9c\c7\dd\f1\c9\e8\24\f0\98\bb\d7\af\ae\d2\90\10\67\df\1e\c1\0a" + }; + }; + record { "ts"; variant { Nat64 = 1_753_344_740_000_000_000 : nat64 } }; + record { + "tx"; + variant { + Map = vec { + record { "amt"; variant { Nat64 = 42_000_000 : nat64 } }; + record { + "from"; + variant { + Array = vec { + variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" }; + } + }; + }; + record { "op"; variant { Text = "burn" } }; + } + }; + }; + } +}; ``` -Example without `btype`: +##### `icrc2_transfer_from` + +**Call parameters:** + ``` -variant { Map = vec { - record { "fee"; variant { Nat = 10 : nat } }; - record { "phash"; variant { Blob = - blob "h,,\97\82\ff.\9cx&l\a2e\e7KFVv\d1\89\beJ\c5\c5\ad,h\5c<\ca\ce\be" - }}; - record { "ts"; variant { Nat = 1_701_109_006_692_276_133 : nat } }; - record { "tx"; variant { Map = vec { - record { "op"; variant { Text = "xfer" } }; - record { "amt"; variant { Nat = 609_618 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; - variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; - }}}; - record { "to"; variant { Array = vec { - variant { Blob = blob " \ef\1f\83Zs\0a?\dc\d5y\e7\ccS\9f\0b\14a\ac\9f\fb\f0bf\f3\a9\c7D\02" }; - variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; - }}}; - }}}; -}}; + icrc2_transfer_from: record { + spender_subaccount: opt blob; + from: Account; + to: Account; + amount: Nat; + fee: opt Nat; + memo: opt Blob; + created_at_time: opt Nat; +} ``` -#### Approve Block Schema +**Regular Transfer** — when the `to` account is not the minting account: + +- `op = "xfer"` +- `from = from` (as passed in the call) +- `spender = [caller]` if `spender_subaccount` is not provided +- `spender = [caller, spender_subaccount]` if provided +- `to = to` +- `amt = amount` +- `fee = fee` if provided +- `memo = memo` if provided +- `ts = created_at_time` if provided + +**Burn Transfer** — when the `to` account is the minting account: + +- `op = "burn"` +- `from = from` (as passed in the call) +- `spender = [caller]` if `spender_subaccount` is not provided +- `spender = [caller, spender_subaccount]` if provided- `amt = amount` +- `fee = fee` if provided +- `memo = memo` if provided +- `ts = created_at_time` if provided -1. the `btype` field MUST be `"2approve"` or `tx.op` field MUST be `"approve"` -2. it MUST contain a field `tx.from: Account` -3. it MUST contain a field `tx.spender: Account` -4. it CAN contain a field `tx.expected_allowance: Nat` if set by the user -5. it CAN contain a field `tx.expires_at: Nat` if set by the user -Example with `btype`: + +#### Example 4: Transfer from approval +This example shows an `icrc2_transfer_from` call where the recipient is a regular user account. Only the required fields are provided: `from`, `to`, and `amount`, and the spender subaccount is omitted (defaults to `null`, i.e., the default subaccount). + ``` -variant { Map = vec { - record { "btype"; "variant" { Text = "2approve" }}; - record { "fee"; variant { Nat = 10 : nat } }; - record { "phash"; variant { - Blob = blob ";\f7\bet\b6\90\b7\ea2\f4\98\a5\b0\60\a5li3\dcXN\1f##2\b5\db\de\b1\b3\02\f5" - }}; - record { "ts"; variant { Nat = 1_701_167_840_950_358_788 : nat } }; - record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat = 18_446_744_073_709_551_615 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\16c\e1\91v\eb\e5)\84:\b2\80\13\cc\09\02\01\a8\03[X\a5\a0\d3\1f\e4\c3{\02" }; - }}}; - record { "spender"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\e0\1dI\01\01" }; - }}}; - }}}; -}}}; +variant { + Map = vec { + record { "fee"; variant { Nat64 = 10_000 : nat64 } }; + record { + "phash"; + variant { + Blob = blob "\a0\5f\d2\f3\4c\26\73\58\00\7f\ea\02\18\43\47\70\85\50\2e\d2\1f\23\e0\dc\e6\af\3c\cf\9e\6f\4a\d8" + }; + }; + record { "ts"; variant { Nat64 = 1_753_344_728_820_625_931 : nat64 } }; + record { + "tx"; + variant { + Map = vec { + record { "amt"; variant { Nat64 = 50_419_165_435 : nat64 } }; + record { + "from"; + variant { + Array = vec { + variant { Blob = blob "\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc\cc" } + } + }; + }; + record { + "spender"; + variant { + Array = vec { + variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" } + } + }; + }; + record { "op"; variant { Text = "xfer" } }; + record { + "to"; + variant { + Array = vec { + variant { Blob = blob "\3a\b2\17\29\53\18\70\89\73\bf\db\61\ed\28\c7\22\dc\63\2e\60\3d\50\cd\6c\9e\36\b2\ef\02" } + } + }; + }; + } + }; + }; + } +}; ``` -Example without `btype`: +--- + +#### Example 5: Burn from approval (to minting account with memo) +This example shows an `icrc2_transfer_from` call where the destination `to` is the minting account, resulting in a burn block. The call includes a `memo`, and no `spender_subaccount` is provided. Therefore, the `spender` field consists only of the caller's principal (default subaccount). This example demonstrates a minimal burn operation initiated via approval, with memo included. + ``` -variant { Map = vec { - record { "fee"; variant { Nat = 10 : nat } }; - record { "phash"; variant { - Blob = blob ";\f7\bet\b6\90\b7\ea2\f4\98\a5\b0\60\a5li3\dcXN\1f##2\b5\db\de\b1\b3\02\f5" - }}; - record { "ts"; variant { Nat = 1_701_167_840_950_358_788 : nat } }; - record { "tx"; variant { Map = vec { - record { "op"; variant { Text = "approve" } }; - record { "amt"; variant { Nat = 18_446_744_073_709_551_615 : nat } }; - record { "from"; variant { Array = vec { - variant { Blob = blob "\16c\e1\91v\eb\e5)\84:\b2\80\13\cc\09\02\01\a8\03[X\a5\a0\d3\1f\e4\c3{\02" }; - }}}; - record { "spender"; variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\e0\1dI\01\01" }; - }}}; - }}}; -}}}; +variant { + Map = vec { + record { + "phash"; + variant { + Blob = blob "\9a\cd\20\3f\b0\11\fb\7f\e2\2a\1d\f2\c1\dd\22\6a\2f\1e\f6\88\d3\b0\9f\be\8d\2e\c5\70\f2\b4\a1\77" + }; + }; + record { "ts"; variant { Nat64 = 1_753_344_750_000_000_000 : nat64 } }; + record { + "tx"; + variant { + Map = vec { + record { "amt"; variant { Nat64 = 200_000 : nat64 } }; + record { + "from"; + variant { + Array = vec { + variant { Blob = blob "\ab\cd\01\23\45\67\89\ab\cd\ef\01\23\45\67\89\ab\cd\ef\01\23\45\67\89\ab\cd\ef\01\23\45\67\89\ab" } + } + }; + }; + record { + "spender"; + variant { + Array = vec { + variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" } + } + }; + }; + record { "op"; variant { Text = "burn" } }; + record { "memo"; variant { Blob = blob "burn by spender" } }; + } + }; + }; + } +}; ``` + + + ## Specification ### `icrc3_get_blocks` From d4d58565235d19ad9523a727e58ac6b11adee51a Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 15 Aug 2025 14:46:47 +0200 Subject: [PATCH 05/38] introduced a section on kinds of blocks --- standards/ICRC-3/README.md | 89 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 3e39bc0..77b458c 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -88,12 +88,93 @@ An ICRC-3 compliant Block 2. MUST contain a field `phash: Blob` which is the hash of its parent if it has a parent block 3. SHOULD contain a field `btype: String` which uniquely describes the type of the Block. If this field is not set then the block type falls back to ICRC-1 and ICRC-2 for backward compatibility purposes -## Interaction with other standards -Each standard that adheres to `ICRC-3` MUST define the list of block schemas that it introduces. Each block schema MUST: +### Kinds of Blocks + +An ICRC-3 block can record different kinds of information. Some blocks record the result of a transaction submitted by a user. These typically contain a `tx` field describing the user’s intent and any parameters they provided. + +Other blocks may be created by the ledger itself, for example during an upgrade, migration, or system operation, to record changes in ledger state that did not come from a user call. + +The `tx` field, when present, encodes the **intent** or **state change payload** associated with the block: +- In user-initiated blocks, `tx` reflects the call parameters, subject to the canonical mapping defined for that block type. +- In system-generated blocks, `tx` may capture the minimal structure required to interpret the block’s meaning and effect, as defined in the specification for that block type. + +The exact meaning of a block and its `tx` structure is determined by its block type. +Block types and their schemas are defined either by legacy standards (e.g., ICRC-1, ICRC-2) or by newer standards introducing `btype`-tagged blocks. + + + +## Principles and Rules for ICRC-3 Blocks + +The following principles guide the evolution and interpretation of ICRC-3 and any standards that build on it. + +### 1. Separation of `btype` and `tx` +- The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its effect on ledger state. +- Standards that introduce a new `btype` must: + - Assign a unique identifier for the `btype`. + - Specify the minimal `tx` structure required for interpreting that block type. + - Define how the block’s effect on ledger state is derived from this minimal structure. +- Standards that define methods producing blocks must: + - Specify which `btype` the method produces. + - Define the **canonical mapping** from method call parameters to the `tx` field of the resulting block. + - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). + +### 2. Avoiding Collisions in `tx` +- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping must include: + - An operation field (`op`) whose value is namespaced using the standard’s number as a prefix, e.g., `122freeze_account`. +- No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. + +### 3. Inclusion of the User Call in `tx` +- The `tx` field must faithfully capture the structure of the user call that triggered the block. +- All call parameters that are part of the method’s canonical mapping must be included exactly as provided by the caller. +- Optional parameters that were not present in the call must be omitted from `tx`. + +### 4. Fee Calculation and Representation +- The **effective fee** associated with a block is determined as follows: + 1. If a top-level `fee` field is present in the block, it is the effective fee charged by the ledger. + 2. Otherwise, if `tx.fee` is present, the effective fee is that value. + 3. Otherwise, the effective fee is `0`. +- `tx.fee` records the fee amount provided by the caller in the request. +- The top-level `fee` records the actual fee charged by the ledger, allowing for ledger-specific fee policies (e.g., dynamic fees, discounts). +- As an optimization, the top-level `fee` may be omitted if it is equal to `tx.fee`. +- This design allows ledgers to implement flexible, policy-driven fee systems without breaking ICRC-3 compatibility. + +### 5. Fee Payer Inference +- The fee payer must be **determinable from the block type alone**. +- When defining a new `btype`, the standard must state explicitly which account pays the fee for blocks of that type. + +### 6. Future-Proofing and Extensibility +- Additional non-semantic fields (e.g., metadata, hashes, references) may be added to `tx` without introducing a new `btype`, provided: + - They do not affect the block’s effect on ledger state. + - They are ignored by block verification and interpretation logic that only relies on the minimal `tx` structure defined by the `btype`. +- Any change to the minimal semantic structure of a block requires introducing a new `btype`. + + + +## Interaction with Other Standards + +Each standard that adheres to `ICRC-3` MUST define the list of block schemas that it introduces. +Each block schema MUST: + +1. Extend the [Generic Block Schema](#generic-block-schema). +2. Specify the expected value of `btype`. This MUST be unique across all standards. +3. Describe the minimal `tx` structure required to interpret the block and determine its effect on ledger state. +4. Define a canonical mapping from the method’s call parameters to the `tx` fields, including only parameters that are explicitly present in the user’s request. + +When defining how calls map to `tx`: +- The `tx` must encode the user’s intent as passed in the method call. +- Optional parameters not provided by the user MUST NOT appear in the `tx`. +- If a top-level `fee` is present, it represents the effective fee charged by the ledger. + If `tx.fee` is present, it represents the fee value specified by the user in the call. + If both are absent, the fee is `0`. +- The fee payer MUST be inferable from the block type alone. +- Additional non-semantic fields may be added without requiring a new block type. + Any new semantic fields that affect ledger state require a new block type definition. + +### Namespacing for Operations + +An ICRC-x standard MUST use namespacing for its operation identifiers using the following scheme, where the ICRC standard’s number acts as a prefix to the name, followed by an operation name that begins with a letter: -1. extend the [Generic Block Schema](#generic-block-schema) -2. specify the expected value of `btype`. This MUST be unique accross all the standards. An ICRC-x standard MUST use namespacing for its op identifiers using the following scheme of using the ICRC standard's number as prefix to the name followed by an operation name that must begin with a letter: ``` op = icrc_number op_name From 825f3f17045ed3ac92fad0ad017043f886946b71 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 15 Aug 2025 16:58:59 +0200 Subject: [PATCH 06/38] added guideliness for defining blocks/cannonical mappings --- standards/ICRC-3/README.md | 51 ++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 77b458c..98df0f2 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -153,23 +153,36 @@ The following principles guide the evolution and interpretation of ICRC-3 and an ## Interaction with Other Standards -Each standard that adheres to `ICRC-3` MUST define the list of block schemas that it introduces. -Each block schema MUST: - -1. Extend the [Generic Block Schema](#generic-block-schema). -2. Specify the expected value of `btype`. This MUST be unique across all standards. -3. Describe the minimal `tx` structure required to interpret the block and determine its effect on ledger state. -4. Define a canonical mapping from the method’s call parameters to the `tx` fields, including only parameters that are explicitly present in the user’s request. - -When defining how calls map to `tx`: -- The `tx` must encode the user’s intent as passed in the method call. -- Optional parameters not provided by the user MUST NOT appear in the `tx`. -- If a top-level `fee` is present, it represents the effective fee charged by the ledger. - If `tx.fee` is present, it represents the fee value specified by the user in the call. - If both are absent, the fee is `0`. -- The fee payer MUST be inferable from the block type alone. -- Additional non-semantic fields may be added without requiring a new block type. - Any new semantic fields that affect ledger state require a new block type definition. +Any standard that builds on ICRC-3 and defines new block types MUST clearly and unambiguously describe how those block types are recorded and interpreted. + +### When Defining a New Block Type +For each block type introduced by a standard, the specification MUST: + +1. **Extend the [Generic Block Schema](#generic-block-schema)**. +2. **Assign a unique `btype` value** that does not collide with any existing standard. +3. **Describe the minimal `tx` structure** required to interpret the block and determine its effect on ledger state. +4. **Specify the fee payer** for this block type. + - The payer MUST be derivable from the block’s own fields (no external context or method-specific exceptions). + - This ensures the payer can be determined unambiguously by any ICRC-3-aware client. +5. **Reference the [ICRC-3 effective fee rule](#principles-and-rules-for-icrc-3-blocks)** and state how it applies to this block type. + - Clarify whether `tx.fee` is expected in user calls, when a top-level `fee` is present, and under what conditions either may be omitted. +6. **Identify optional non-semantic fields** that may be added to `tx` without requiring a new block type. + - Any change to the minimal semantic structure that affects ledger state requires defining a new `btype`. + +### When Defining a Canonical Mapping from Methods to Blocks +For each method that produces a block: + +1. **Identify the `btype`** the method produces. +2. **Define the canonical mapping** from the method’s call parameters to the `tx` fields of the resulting block: + - Include only parameters explicitly provided by the caller. + - Optional parameters not provided MUST NOT appear in `tx`. +3. **Record fees according to the ICRC-3 fee rules**: + - `tx.fee` records the fee amount provided by the caller in the request. + - A top-level `fee` records the actual fee charged by the ledger. + - If both are absent, the fee is `0`. +4. **Ensure operation names are namespaced** using the standard’s ICRC number as a prefix (see [Namespacing for Operations](#namespacing-for-operations)). + +This separation ensures that block semantics and method mappings are both specified with clarity and without cross-contamination. ### Namespacing for Operations @@ -191,7 +204,9 @@ An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported bl ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema -This section describes how ICRC-1 and ICRC-2 operations are recorded in ICRC-3-compliant blocks. + +This section describes how ICRC-1 and ICRC-2 operations are represented in ICRC-3-compliant blocks. These blocks follow the **legacy format**, meaning they do not have a `btype` field. +Instead, their type is inferred directly from the content of the `tx` field, which records the canonical mapping of the original method call. From 9f7a603ae430c744b4b7a2001c0aa4ee081d7c47 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Wed, 27 Aug 2025 16:25:47 +0200 Subject: [PATCH 07/38] core state transition, bypte vs tx --- standards/ICRC-3/README.md | 306 ++++++++++++++++++++++++++----------- 1 file changed, 217 insertions(+), 89 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 3912784..72f7b72 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -108,99 +108,122 @@ Block types and their schemas are defined either by legacy standards (e.g., ICRC The following principles guide the evolution and interpretation of ICRC-3 and any standards that build on it. -### 1. Separation of `btype` and `tx` -- The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its effect on ledger state. +### 1. Core State Transitions +- Every block type must define the **core state transition** it represents: the deterministic change to ledger state implied by the block’s minimal `tx` structure, *ignoring fees or ledger-specific policies*. +- This transition is the canonical meaning of a block — what balances, allowances, or other state variables change as a direct consequence of the block. +- Fee handling, metadata, and ledger-specific policies are layered on top of this transition. + +### 2. Separation of `btype` and `tx` +- The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its core state transition. - Standards that introduce a new `btype` must: - Assign a unique identifier for the `btype`. - Specify the minimal `tx` structure required for interpreting that block type. - - Define how the block’s effect on ledger state is derived from this minimal structure. + - Define the block’s **core state transition** in terms of this minimal structure. - Standards that define methods producing blocks must: - Specify which `btype` the method produces. - Define the **canonical mapping** from method call parameters to the `tx` field of the resulting block. - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). -### 2. Avoiding Collisions in `tx` +### 3. Avoiding Collisions in `tx` - To avoid collisions between transactions originating from different standards, the canonical `tx` mapping must include: - An operation field (`op`) whose value is namespaced using the standard’s number as a prefix, e.g., `122freeze_account`. - No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. -### 3. Inclusion of the User Call in `tx` +### 4. Inclusion of the User Call in `tx` - The `tx` field must faithfully capture the structure of the user call that triggered the block. - All call parameters that are part of the method’s canonical mapping must be included exactly as provided by the caller. - Optional parameters that were not present in the call must be omitted from `tx`. -### 4. Fee Calculation and Representation -- The **effective fee** associated with a block is determined as follows: - 1. If a top-level `fee` field is present in the block, it is the effective fee charged by the ledger. - 2. Otherwise, if `tx.fee` is present, the effective fee is that value. - 3. Otherwise, the effective fee is `0`. -- `tx.fee` records the fee amount provided by the caller in the request. -- The top-level `fee` records the actual fee charged by the ledger, allowing for ledger-specific fee policies (e.g., dynamic fees, discounts). -- As an optimization, the top-level `fee` may be omitted if it is equal to `tx.fee`. -- This design allows ledgers to implement flexible, policy-driven fee systems without breaking ICRC-3 compatibility. - -### 5. Fee Payer Inference -- The fee payer must be **determinable from the block type alone**. -- When defining a new `btype`, the standard must state explicitly which account pays the fee for blocks of that type. - -### 6. Future-Proofing and Extensibility +### 5. Future-Proofing and Extensibility - Additional non-semantic fields (e.g., metadata, hashes, references) may be added to `tx` without introducing a new `btype`, provided: - - They do not affect the block’s effect on ledger state. + - They do not affect the block’s **core state transition**. - They are ignored by block verification and interpretation logic that only relies on the minimal `tx` structure defined by the `btype`. - Any change to the minimal semantic structure of a block requires introducing a new `btype`. +### Note on Ledger-Specific Fields +- Blocks may include additional fields specific to a given standard or ledger (e.g., `fee`, metadata, references). +- ICRC-3 defines how such fields are recorded and verified, but **does not define their economic or behavioral semantics**. Those semantics must be specified by the standard that introduces the block type (e.g., fee rules in ICRC-107). + +## Semantics of Blocks: Evaluation Model + +To ensure consistency across standards and implementations, the semantics of any block must be interpretable through the following evaluation model. Each standard that defines a block type specifies how to “plug into” this model (by defining its minimal `tx` schema, pre-fee transition, fee payer, etc.). + +1. Identify block type + • If `btype` is present, use it. + • If no `btype`, fall back to legacy ICRC-1/2 inference from `tx.op`. + +2. Validate `tx` structure + • Check that `tx` includes all required fields defined for the block type. + • Ensure no extra *semantic* fields beyond those defined by the block type are present. + • Optional caller-provided fields may appear if allowed by the canonical mapping. + +3. Derive pre-fee state transition + • Apply the deterministic state change implied by `tx`, ignoring any fees. + • Example: debit/credit balances, mint, burn, update allowance. + +4. Apply fee (if applicable) + • If the block type involves fees, determine the **effective fee** following ICRC-107 rules. + • Deduct the fee from the account designated as the **fee payer** for this block type. + • Adjust balances accordingly (e.g., for mints: `to` receives `amt – fee`). + +5. Enforce validity conditions + • Ensure balances remain non-negative. + • Verify sufficient funds to cover `amt + fee` (where applicable). + • Require `fee ≤ amt` for mint blocks. + • Enforce any invariants specified by the block type’s standard. ## Interaction with Other Standards -Any standard that builds on ICRC-3 and defines new block types MUST clearly and unambiguously describe how those block types are recorded and interpreted. +ICRC-3 defines how blocks are structured and verified. Other standards extend this by either: +(1) introducing new block types (`btype`), or +(2) defining canonical mappings from standardized method calls to existing block types. -### When Defining a New Block Type -For each block type introduced by a standard, the specification MUST: +### Standards That Introduce Block Types +A standard that defines a new block type must: +- Assign a unique `btype`. +- Specify the minimal `tx` structure required to interpret the block and determine its effect on ledger state. +- Define semantics using the **Semantics of Blocks: Evaluation Model** (pre-fee transition, fee hook, post-conditions). +- If the block type involves fees, reference the applicable fee standard (e.g., ICRC-107) and **define who pays**, via a fee payer expression resolvable from block fields. -1. **Extend the [Generic Block Schema](#generic-block-schema)**. -2. **Assign a unique `btype` value** that does not collide with any existing standard. -3. **Describe the minimal `tx` structure** required to interpret the block and determine its effect on ledger state. -4. **Specify the fee payer** for this block type. - - The payer MUST be derivable from the block’s own fields (no external context or method-specific exceptions). - - This ensures the payer can be determined unambiguously by any ICRC-3-aware client. -5. **Reference the [ICRC-3 effective fee rule](#principles-and-rules-for-icrc-3-blocks)** and state how it applies to this block type. - - Clarify whether `tx.fee` is expected in user calls, when a top-level `fee` is present, and under what conditions either may be omitted. -6. **Identify optional non-semantic fields** that may be added to `tx` without requiring a new block type. - - Any change to the minimal semantic structure that affects ledger state requires defining a new `btype`. +### Standards That Define Methods +A standard that defines a method which produces blocks must: +- Specify which `btype` (if any) the method produces. +- Define the canonical mapping from method inputs to the `tx` field of the resulting block. +- Ensure all required fields from the block type’s minimal schema are populated. +- Include only caller-provided optional fields; omit optionals that were not supplied. +- Include an `op` field in `tx` to identify the operation and avoid collisions. -### When Defining a Canonical Mapping from Methods to Blocks -For each method that produces a block: +This division of responsibility ensures that: +- Block types define **what blocks mean** (semantics). +- Methods define **how blocks are created** (intent capture). +- Tooling and clients can rely on predictable, non-colliding `tx` values. -1. **Identify the `btype`** the method produces. -2. **Define the canonical mapping** from the method’s call parameters to the `tx` fields of the resulting block: - - Include only parameters explicitly provided by the caller. - - Optional parameters not provided MUST NOT appear in `tx`. -3. **Record fees according to the ICRC-3 fee rules**: - - `tx.fee` records the fee amount provided by the caller in the request. - - A top-level `fee` records the actual fee charged by the ledger. - - If both are absent, the fee is `0`. -4. **Ensure operation names are namespaced** using the standard’s ICRC number as a prefix (see [Namespacing for Operations](#namespacing-for-operations)). -This separation ensures that block semantics and method mappings are both specified with clarity and without cross-contamination. +#### Namespacing for Operations +To avoid collisions across standards, `tx.op` MUST be namespaced: +- `op = icrc_number op_name` +- `icrc_number`: a non-zero digit followed by zero or more digits +- `op_name`: starts with a lowercase letter, then lowercase letters, digits, `_` or `-` +**Examples:** `1transfer`, `2transfer_from`, `123freeze_account`. -### Namespacing for Operations -An ICRC-x standard MUST use namespacing for its operation identifiers using the following scheme, where the ICRC standard’s number acts as a prefix to the name, followed by an operation name that begins with a letter: -``` -op = icrc_number op_name -icrc_number = nonzero_digit *digit -nonzero_digit = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" -digit = "0" / nonzero_digit -op_name = a-z *(a-z / digit / "_" / "-") -``` + +### Note on Fees +ICRC-3 itself does not define fee semantics. +Standards that define block types which involve fees must follow the principles and rules specified in **ICRC-107 (Fee Handling in Blocks)**. +ICRC-3 only requires that the fee payer for a block type be clearly defined, so that fee responsibility is unambiguous. ## Supported Standards -An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. The Ledger MUST return only blocks with `btype` set to one of the values returned by this endpoint. +An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. + +- For **typed** blocks, the ledger MUST only produce blocks whose `"btype"` value is included in this list. +- For **legacy** ICRC-1/2 blocks (no `"btype"`), the ledger MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. + ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema @@ -208,40 +231,115 @@ An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported bl This section describes how ICRC-1 and ICRC-2 operations are represented in ICRC-3-compliant blocks. These blocks follow the **legacy format**, meaning they do not have a `btype` field. Instead, their type is inferred directly from the content of the `tx` field, which records the canonical mapping of the original method call. +### Legacy ICRC-1 and ICRC-2 Block Structure +ICRC-1 and ICRC-2 blocks **MUST NOT** include a `btype` field. These standards use the legacy block format where the block type is determined exclusively from the content of the `tx` field. -### No `btype` Field -ICRC-1 and ICRC-2 blocks MUST NOT include a `btype` field. These standards use the legacy block format where the type of block is determined exclusively by the content of the `tx` field. ICRC-1 and ICRC-2 blocks use the `tx` field to store input from the user and use the external block to store data set by the Ledger. For instance, the amount of a transaction is stored in the field `tx.amt` because it has been specified by the user, while the time when the block was added to the Ledger is stored in the field `ts` because it is set by the Ledger. +Legacy blocks therefore follow a fixed generic structure, with semantics inferred from `tx.op`. -#### Block Structure +--- -A generic ICRC-1 or ICRC-2 Block: +#### Generic Legacy Block -- **MUST** use the legacy format, i.e., it **MUST NOT** include a `"btype"` field. -- **MUST** be a `Value::Map` containing at least the following fields: - - `"phash"`: `Blob` — the hash of the parent block. - - `"ts"`: `Nat` — the timestamp (set by the ledger at block creation). +A legacy block: + +- **MUST** be a `Value::Map` containing at least: + - `"phash"`: `Blob` — the parent hash. + - `"ts"`: `Nat` — the timestamp set by the ledger when the block was created. - `"tx"`: `Value::Map` — representing the user’s transaction intent. -- **CAN** include: - - `"fee"`: `Nat` — only if the ledger requires a fee and the user did not specify one in `tx.fee`. +- **MAY** include: + - `"fee": Nat` — the fee actually charged by the ledger, if any. + +--- + +#### Transfer Block (`op = "xfer"`) + +**Structure** +- **MUST** contain `tx.op = "xfer"`. +- **MUST** contain `tx.from : Account`. +- **MUST** contain `tx.to : Account`. +- **MUST** contain `tx.amt : Nat`. +- **MAY** contain `tx.fee : Nat` if provided by the caller. +- **MAY** contain `tx.memo : Blob` if provided by the caller. +- **MAY** contain `tx.ts : Nat` if provided by the caller (`created_at_time`). +- **MAY** contain `tx.spender : Account` if created via `icrc2_transfer_from`. + +**Semantics** +Transfers debit `amt` (and any fee) from `from` and credit `amt` to `to`. +If `tx.spender` is present, the operation is executed under an approval, which must cover at least `amt + fee`. The allowance is reduced accordingly. + +**Fee payer:** `from`. + +--- + +#### Mint Block (`op = "mint"`) + +**Structure** +- **MUST** contain `tx.op = "mint"`. +- **MUST** contain `tx.to : Account`. +- **MUST** contain `tx.amt : Nat`. +- **MUST NOT** contain `tx.from`. +- **MAY** contain `tx.memo : Blob` if provided by the caller. +- **MAY** contain `tx.ts : Nat` if provided by the caller. + +**Semantics** +Mints create `amt` new tokens. If a fee is charged, it is deducted from `to` immediately, so `to` receives `amt - fee` (require `fee ≤ amt`). + +**Fee payer:** `to`. +--- + +#### Burn Block (`op = "burn"`) + +**Structure** +- **MUST** contain `tx.op = "burn"`. +- **MUST** contain `tx.from : Account`. +- **MUST** contain `tx.amt : Nat`. +- **MUST NOT** contain `tx.to`. +- **MAY** contain `tx.fee : Nat` if provided by the caller. +- **MAY** contain `tx.memo : Blob` if provided by the caller. +- **MAY** contain `tx.ts : Nat` if provided by the caller. + +**Semantics** +Burns remove `amt` tokens from `from`. Any fee is also debited from `from`. + +**Fee payer:** `from`. + +--- -#### `tx` Field Semantics +#### Approve Block (`op = "approve"`) -The `tx` field: +**Structure** +- **MUST** contain `tx.op = "approve"`. +- **MUST** contain `tx.from : Account`. +- **MUST** contain `tx.spender : Account`. +- **MUST** contain the allowance field as defined by ICRC-2 (e.g., `tx.amt : Nat`). +- **MAY** contain `tx.fee : Nat` if provided by the caller. +- **MAY** contain `tx.memo : Blob` if provided by the caller. +- **MAY** contain `tx.ts : Nat` if provided by the caller. -- **MUST** represent the user intent derived from the method call. -- **MUST** be encoded using the ICRC-3 `Value` type. -- **MUST NOT** contain any fields that were not explicitly present in the original user call. -- **MUST** follow the canonical mapping rules described in the next section. -- **MUST** contain a field `op: String` with value one of "mint", "burn", "xfer", "approve" -- **MUST** contain a field `amt: Nat` that represents the amount -- **MUST** contain the `fee: Nat` field for operations that require a fee if the user specifies the fee in the request. If the user does not specify the fee in the request, then this field is not set and the top-level `fee` is set. - - **CAN** contain the `memo: Blob` field if specified by the user - - **CAN** contain the `ts: Nat` field if the user sets the `created_at_time` field in the request. +**Semantics** +Approvals set or update the allowance of `spender` on `from`. +Any subsequent `xfer` block with `tx.spender` consumes the allowance. +Fees (if any) are debited from `from`. + +**Fee payer:** `from`. + +--- + +#### Notes on Fee Representation (Legacy Blocks) + +- The **effective fee** for a block is computed as: + 1. If a top-level `"fee"` is present, that is the fee charged by the ledger. + 2. Otherwise, if `tx.fee` is present, the effective fee equals `tx.fee`. + 3. Otherwise, the fee is `0`. + +- `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. +- If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. +- Ledgers **may** omit the top-level `"fee"` when it equals `tx.fee` to save space. +- The **destination/handling** of the fee (e.g., sink account, burn) is specified by the fee standard (see **ICRC-107, Fee Handling in Blocks**); ICRC-3 only standardizes how fees are recorded in blocks, not where they go. -Operations that require paying a fee: Transfer, and Approve. ### Compliance Reporting @@ -321,7 +419,7 @@ icrc1_transfer: record { - `amt = amount` - `memo = memo` if provided - `ts = created_at_time` if provided -> `from` and `fee` MUST NOT be present +- `from` and `fee` MUST NOT be present **Transfer to the Minting Account (→ Burn)** — when `to` equals the minting account: @@ -331,24 +429,51 @@ icrc1_transfer: record { - `amt = amount` - `memo = memo` if provided - `ts = created_at_time` if provided -> `to` and `fee` MUST NOT be present +- `to` and `fee` MUST NOT be present + + +### Fee Payer and Balance Effects (Legacy ICRC-1/2) + +The rules below define **who pays** and how the **effective fee** (if any) affects balances for legacy blocks (no `btype`; kind inferred from `tx`). The authoritative charged amount is the top-level `fee : Nat` when present; `tx.fee` (if present) reflects the caller input only. + +#### `icrc1_transfer` +- `op = "xfer"` → **Payer:** `from` + • Debited from `from`: `amt + fee` (if a fee is charged) + • Credited to `to`: `amt` +- `op = "burn"` → **Payer:** `from` + • Debited from `from`: `amt + fee` (if a fee is charged) + • Burned: `amt` +- `op = "mint"` → **Payer:** `to` + • Credited to `to`: `amt - fee` (if a fee is charged; require `fee ≤ amt`) + • Minted gross amount: `amt` (with `fee` immediately taken from `to`) + +#### `icrc2_transfer_from` +- `op = "xfer"` → **Payer:** `from` (authorized by `spender`) + • Debited from `from`: `amt + fee` (if a fee is charged) + • Credited to `to`: `amt` +- `op = "burn"` → **Payer:** `from` (authorized by `spender`) + • Debited from `from`: `amt + fee` (if a fee is charged) + • Burned: `amt` + +#### `icrc2_approve` +- **Payer:** `from` (the account whose allowance is modified) + • Debited from `from`: `fee` (if a fee is charged) + +**Notes** +- A fee may be charged even if `tx.fee` is absent; the charged fee is indicated by a top-level `fee`. +- If no top-level `fee` is present, the effective fee is `0`. +- Implementations must reject calls that cannot satisfy the fee rule (e.g., `fee > amt` for mint; or insufficient balance for `amt + fee` debits). + ### Canonical Examples of `icrc1_transfer` Blocks Each of the following examples represents a canonical block resulting from an `icrc1_transfer` call. These examples illustrate different scenarios depending on which optional fields were included in the call. Only parameters explicitly provided by the caller appear in the resulting `tx`. ---- + #### Example 1: Transfer with required parameters only This example shows an `icrc1_transfer` call where the caller only specifies the mandatory fields: `to` and `amount`. No `memo`, `created_at_time`, or explicit `fee` are provided. The block still contains a top-level `fee` field since the ledger applies the default transfer fee. -### Burn Block Schema - -1. the `btype` MUST NOT be set and `tx.op` field MUST be `"burn"` -2. it MUST contain a field `tx.from: Account` -3. it MUST contain a field `tx.amt: Nat` -4. it MUST contain a field `tx.memo` if the `icrc1_transfer` call that creates the block has a memo field, and its value is the value of that field. - ``` variant { @@ -483,18 +608,21 @@ variant { - `memo = memo` if provided - `ts = created_at_time` if provided + **Burn Transfer** — when the `to` account is the minting account: - `op = "burn"` - `from = from` (as passed in the call) - `spender = [caller]` if `spender_subaccount` is not provided -- `spender = [caller, spender_subaccount]` if provided- `amt = amount` +- `spender = [caller, spender_subaccount]` if provided +- `amt = amount` - `fee = fee` if provided - `memo = memo` if provided - `ts = created_at_time` if provided + #### Example 4: Transfer from approval This example shows an `icrc2_transfer_from` call where the recipient is a regular user account. Only the required fields are provided: `from`, `to`, and `amount`, and the spender subaccount is omitted (defaults to `null`, i.e., the default subaccount). From 79018dcc9951386a627b65a6f8f749138b632402 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Mon, 1 Sep 2025 13:03:40 +0200 Subject: [PATCH 08/38] typos --- standards/ICRC-3/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 72f7b72..a367420 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -50,7 +50,7 @@ type Value = variant { }; ``` -Servers must serve the block log as a list of `Value` where each `Value` represent a single block in the block log. +Servers must serve the block log as a list of `Value` where each `Value` represents a single block in the block log. ## Value Hash @@ -66,15 +66,15 @@ The hash function is the [representation-independent hashing of structured data] - the hash of an `Array` is the hash of the concatenation of the hashes of all the elements of the array - the hash of a `Map` is the hash of the concatenation of all the hashed items of the map sorted lexicographically. A hashed item is the tuple composed by the hash of the key and the hash of the value. -Pseudocode for representation independent hashing of Value, together with test vectors to check compliance with the specification can be found [`here`](HASHINGVALUES.md). +Pseudocode for representation-independent hashing of `Value`, together with test vectors to check compliance with the specification can be found [`here`](HASHINGVALUES.md). ## Blocks Verification -The Ledger MUST certify the last block (tip) recorded. The Ledger MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labelled values (leafs): -1. `last_block_index`: the index of the last block in the chain. The values must be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) +The Ledger MUST certify the last block (tip) recorded. The Ledger MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): +1. `last_block_index`: the index of the last block in the chain. The values MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain -Clients SHOULD download the tip certificate first and then download the block backward starting from `last_block_index` and validate the blocks in the process. +Clients SHOULD download the tip certificate first and then download the blocks backward starting from `last_block_index` and validate the blocks in the process. Validation of block `i` is done by checking the block hash against 1. if `i + 1 < len(chain)` then the parent hash `phash` of the block `i+1` @@ -86,7 +86,7 @@ An ICRC-3 compliant Block 1. MUST be a `Value` of variant `Map` 2. MUST contain a field `phash: Blob` which is the hash of its parent if it has a parent block -3. SHOULD contain a field `btype: String` which uniquely describes the type of the Block. If this field is not set then the block type falls back to ICRC-1 and ICRC-2 for backward compatibility purposes +3. SHOULD contain a field `btype: Text` which uniquely describes the type of the Block. If this field is not set then the block type falls back to ICRC-1 and ICRC-2 for backward compatibility purposes ### Kinds of Blocks From 4e88917eceb63d146719084455055a733bcc2541 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Mon, 1 Sep 2025 16:52:05 +0200 Subject: [PATCH 09/38] minor changes --- standards/ICRC-3/README.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index a367420..0db07eb 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -50,13 +50,13 @@ type Value = variant { }; ``` -Servers must serve the block log as a list of `Value` where each `Value` represents a single block in the block log. +Servers MUST serve the block log as a list of `Value` where each `Value` represents a single block in the block log. ## Value Hash `ICRC-3` specifies a standard hash function over `Value`. -This hash function should be used by Ledgers to calculate the hash of the parent of a block and by clients to verify the downloaded block log. +This hash function SHOULD be used by Ledgers to calculate the hash of the parent of a block and by clients to verify the downloaded block log. The hash function is the [representation-independent hashing of structured data](https://internetcomputer.org/docs/current/references/ic-interface-spec#hash-of-map) used by the IC: - the hash of a `Blob` is the hash of the bytes themselves @@ -109,36 +109,36 @@ Block types and their schemas are defined either by legacy standards (e.g., ICRC The following principles guide the evolution and interpretation of ICRC-3 and any standards that build on it. ### 1. Core State Transitions -- Every block type must define the **core state transition** it represents: the deterministic change to ledger state implied by the block’s minimal `tx` structure, *ignoring fees or ledger-specific policies*. +- Every block type MUST define the **core state transition** it represents: the deterministic change to ledger state implied by the block’s minimal `tx` structure, *ignoring fees or ledger-specific policies*. - This transition is the canonical meaning of a block — what balances, allowances, or other state variables change as a direct consequence of the block. - Fee handling, metadata, and ledger-specific policies are layered on top of this transition. ### 2. Separation of `btype` and `tx` - The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its core state transition. -- Standards that introduce a new `btype` must: +- Standards that introduce a new `btype` MUST: - Assign a unique identifier for the `btype`. - Specify the minimal `tx` structure required for interpreting that block type. - Define the block’s **core state transition** in terms of this minimal structure. -- Standards that define methods producing blocks must: +- Standards that define methods producing blocks MUST: - Specify which `btype` the method produces. - Define the **canonical mapping** from method call parameters to the `tx` field of the resulting block. - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). ### 3. Avoiding Collisions in `tx` -- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping must include: +- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping MUST include: - An operation field (`op`) whose value is namespaced using the standard’s number as a prefix, e.g., `122freeze_account`. - No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. ### 4. Inclusion of the User Call in `tx` - The `tx` field must faithfully capture the structure of the user call that triggered the block. -- All call parameters that are part of the method’s canonical mapping must be included exactly as provided by the caller. -- Optional parameters that were not present in the call must be omitted from `tx`. +- All call parameters that are part of the method’s canonical mapping MUST be included exactly as provided by the caller. +- Optional parameters that were not present in the call MUST be omitted from `tx`. ### 5. Future-Proofing and Extensibility -- Additional non-semantic fields (e.g., metadata, hashes, references) may be added to `tx` without introducing a new `btype`, provided: +- Additional non-semantic fields (e.g., metadata, hashes, references) MAY be added to `tx` without introducing a new `btype`, provided: - They do not affect the block’s **core state transition**. - They are ignored by block verification and interpretation logic that only relies on the minimal `tx` structure defined by the `btype`. -- Any change to the minimal semantic structure of a block requires introducing a new `btype`. +- Any change to the minimal semantic structure of a block REQUIRES introducing a new `btype`. ### Note on Ledger-Specific Fields - Blocks may include additional fields specific to a given standard or ledger (e.g., `fee`, metadata, references). @@ -162,7 +162,7 @@ To ensure consistency across standards and implementations, the semantics of any • Example: debit/credit balances, mint, burn, update allowance. 4. Apply fee (if applicable) - • If the block type involves fees, determine the **effective fee** following ICRC-107 rules. + • If the block type involves fees, determine the **effective fee** following ICRC-107. • Deduct the fee from the account designated as the **fee payer** for this block type. • Adjust balances accordingly (e.g., for mints: `to` receives `amt – fee`). @@ -180,14 +180,14 @@ ICRC-3 defines how blocks are structured and verified. Other standards extend th (2) defining canonical mappings from standardized method calls to existing block types. ### Standards That Introduce Block Types -A standard that defines a new block type must: +A standard that defines a new block type MUST: - Assign a unique `btype`. - Specify the minimal `tx` structure required to interpret the block and determine its effect on ledger state. - Define semantics using the **Semantics of Blocks: Evaluation Model** (pre-fee transition, fee hook, post-conditions). - If the block type involves fees, reference the applicable fee standard (e.g., ICRC-107) and **define who pays**, via a fee payer expression resolvable from block fields. ### Standards That Define Methods -A standard that defines a method which produces blocks must: +A standard that defines a method which produces blocks MUST: - Specify which `btype` (if any) the method produces. - Define the canonical mapping from method inputs to the `tx` field of the resulting block. - Ensure all required fields from the block type’s minimal schema are populated. @@ -213,7 +213,7 @@ To avoid collisions across standards, `tx.op` MUST be namespaced: ### Note on Fees ICRC-3 itself does not define fee semantics. -Standards that define block types which involve fees must follow the principles and rules specified in **ICRC-107 (Fee Handling in Blocks)**. +Standards that define block types which involve fees MUST follow **ICRC-107 (Fee Handling in Blocks)**. ICRC-3 only requires that the fee payer for a block type be clearly defined, so that fee responsibility is unambiguous. @@ -336,8 +336,8 @@ Fees (if any) are debited from `from`. - `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. - If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. -- Ledgers **may** omit the top-level `"fee"` when it equals `tx.fee` to save space. -- The **destination/handling** of the fee (e.g., sink account, burn) is specified by the fee standard (see **ICRC-107, Fee Handling in Blocks**); ICRC-3 only standardizes how fees are recorded in blocks, not where they go. +- Ledgers **MAY** omit the top-level `"fee"` when it equals `tx.fee` to save space. +- The **destination/handling** of the fee (e.g., collecting account, burn) is specified by the fee standard (see **ICRC-107, Fee Handling in Blocks**); ICRC-3 only standardizes how fees are recorded in blocks, not where they go. From 164f310ad4f4436d3b9a6b9c6b39a8276c684dfd Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Wed, 3 Sep 2025 18:19:06 +0200 Subject: [PATCH 10/38] fixed some corner cases --- standards/ICRC-3/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 0db07eb..c7afc8b 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -279,11 +279,15 @@ If `tx.spender` is present, the operation is executed under an approval, which m - **MUST** contain `tx.to : Account`. - **MUST** contain `tx.amt : Nat`. - **MUST NOT** contain `tx.from`. +- **MAY** contain `tx.spender : Account`. - **MAY** contain `tx.memo : Blob` if provided by the caller. - **MAY** contain `tx.ts : Nat` if provided by the caller. **Semantics** Mints create `amt` new tokens. If a fee is charged, it is deducted from `to` immediately, so `to` receives `amt - fee` (require `fee ≤ amt`). +If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `amt + fee` and **MUST** be reduced by `amt + fee`. + + **Fee payer:** `to`. @@ -322,6 +326,8 @@ Burns remove `amt` tokens from `from`. Any fee is also debited from `from`. Approvals set or update the allowance of `spender` on `from`. Any subsequent `xfer` block with `tx.spender` consumes the allowance. Fees (if any) are debited from `from`. +If the approval is set on the minting account, it can be consumed by `icrc2_transfer_from` mints; such mints reduce the allowance by `amt + fee`. + **Fee payer:** `from`. @@ -454,6 +460,9 @@ The rules below define **who pays** and how the **effective fee** (if any) affec - `op = "burn"` → **Payer:** `from` (authorized by `spender`) • Debited from `from`: `amt + fee` (if a fee is charged) • Burned: `amt` +- `op = "mint"` → Payer: to (authorized by spender under an approval on the minting account) +• Credited to `to: amt - fee` (if a fee is charged; require `fee ≤ amt`) +• Allowance on the minting account (for spender) is reduced by `amt + fee` #### `icrc2_approve` - **Payer:** `from` (the account whose allowance is modified) @@ -621,6 +630,16 @@ variant { - `ts = created_at_time` if provided +**Mint Transfer** — when the `from` account is the minting account: + +- `op = "mint"` +- `spender = [caller]` if `spender_subaccount` is not provided +- `spender = [caller, spender_subaccount]` if provided +- `to = to` +- `amt = amount` +- `memo = memo` if provided +- `ts = created_at_time` if provided +- `from` and `fee` **MUST NOT** be present #### Example 4: Transfer from approval From ec99a9dc61004597d38e6ee50667c3958093917b Mon Sep 17 00:00:00 2001 From: bogwar <51327868+bogwar@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:18:27 +0200 Subject: [PATCH 11/38] Update standards/ICRC-3/README.md Co-authored-by: Andrea Cerulli <19587477+andreacerulli@users.noreply.github.com> --- standards/ICRC-3/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index c7afc8b..aa97459 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -335,10 +335,10 @@ If the approval is set on the minting account, it can be consumed by `icrc2_tran #### Notes on Fee Representation (Legacy Blocks) -- The **effective fee** for a block is computed as: - 1. If a top-level `"fee"` is present, that is the fee charged by the ledger. +- The **effective fee** is the fee charged by the ledger. For a block this is computed as: + 1. If a top-level `"fee"` is present, that is the effective fee. 2. Otherwise, if `tx.fee` is present, the effective fee equals `tx.fee`. - 3. Otherwise, the fee is `0`. + 3. Otherwise, the effective fee is `0`. - `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. - If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. From 570e13ccbd74a31c64450cee61a097e6f2763e69 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 5 Sep 2025 17:26:12 +0200 Subject: [PATCH 12/38] timestampe is nanoseconds --- standards/ICRC-3/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index c7afc8b..31296ec 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -245,7 +245,7 @@ A legacy block: - **MUST** be a `Value::Map` containing at least: - `"phash"`: `Blob` — the parent hash. - - `"ts"`: `Nat` — the timestamp set by the ledger when the block was created. + - `"ts"`: `Nat` — the timestamp (in nanoseconds since Unix epoch) set by the ledger when the block was created. - `"tx"`: `Value::Map` — representing the user’s transaction intent. - **MAY** include: - `"fee": Nat` — the fee actually charged by the ledger, if any. From 231779c6551fb75d08c6ca6b15d4f6d2dcd912e1 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 5 Sep 2025 17:55:35 +0200 Subject: [PATCH 13/38] removed the consolidation section --- standards/ICRC-3/README.md | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 8dec8ec..5147bf3 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -438,41 +438,6 @@ icrc1_transfer: record { - `to` and `fee` MUST NOT be present -### Fee Payer and Balance Effects (Legacy ICRC-1/2) - -The rules below define **who pays** and how the **effective fee** (if any) affects balances for legacy blocks (no `btype`; kind inferred from `tx`). The authoritative charged amount is the top-level `fee : Nat` when present; `tx.fee` (if present) reflects the caller input only. - -#### `icrc1_transfer` -- `op = "xfer"` → **Payer:** `from` - • Debited from `from`: `amt + fee` (if a fee is charged) - • Credited to `to`: `amt` -- `op = "burn"` → **Payer:** `from` - • Debited from `from`: `amt + fee` (if a fee is charged) - • Burned: `amt` -- `op = "mint"` → **Payer:** `to` - • Credited to `to`: `amt - fee` (if a fee is charged; require `fee ≤ amt`) - • Minted gross amount: `amt` (with `fee` immediately taken from `to`) - -#### `icrc2_transfer_from` -- `op = "xfer"` → **Payer:** `from` (authorized by `spender`) - • Debited from `from`: `amt + fee` (if a fee is charged) - • Credited to `to`: `amt` -- `op = "burn"` → **Payer:** `from` (authorized by `spender`) - • Debited from `from`: `amt + fee` (if a fee is charged) - • Burned: `amt` -- `op = "mint"` → Payer: to (authorized by spender under an approval on the minting account) -• Credited to `to: amt - fee` (if a fee is charged; require `fee ≤ amt`) -• Allowance on the minting account (for spender) is reduced by `amt + fee` - -#### `icrc2_approve` -- **Payer:** `from` (the account whose allowance is modified) - • Debited from `from`: `fee` (if a fee is charged) - -**Notes** -- A fee may be charged even if `tx.fee` is absent; the charged fee is indicated by a top-level `fee`. -- If no top-level `fee` is present, the effective fee is `0`. -- Implementations must reject calls that cannot satisfy the fee rule (e.g., `fee > amt` for mint; or insufficient balance for `amt + fee` debits). - ### Canonical Examples of `icrc1_transfer` Blocks From d2e6f3e73d94cca69e7a91d03dbf44a802dfb868 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Mon, 15 Sep 2025 16:22:21 +0200 Subject: [PATCH 14/38] clarified tx.op includes standard number --- standards/ICRC-3/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 5147bf3..74badfb 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -125,9 +125,9 @@ The following principles guide the evolution and interpretation of ICRC-3 and an - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). ### 3. Avoiding Collisions in `tx` -- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping MUST include: - - An operation field (`op`) whose value is namespaced using the standard’s number as a prefix, e.g., `122freeze_account`. - No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. +- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping MUST include: +- An operation field (`op`) whose value is namespaced using as prefix the number of the standard that introduces the method, e.g., `122freeze_account`. ### 4. Inclusion of the User Call in `tx` - The `tx` field must faithfully capture the structure of the user call that triggered the block. From df78e30d6512fb26d7dbfc2aa27eb1190e86dfee Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 18 Sep 2025 14:43:06 +0200 Subject: [PATCH 15/38] promoted effective fee to a first class citizen; minor typos and rewrites --- standards/ICRC-3/README.md | 100 +++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 74badfb..52a2a6c 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -10,7 +10,7 @@ 1. A way to fetch the archive nodes of a Ledger 2. A generic format for sharing the block log without information loss. This includes the fields that a block must have 3. A mechanism to verify the block log on the client side to allow downloading the block log via query calls -4. A way for new standards to define new transactions types compatible with ICRC-3 +4. A way for new standards to define new transaction types compatible with ICRC-3 ## Archive Nodes @@ -71,7 +71,7 @@ Pseudocode for representation-independent hashing of `Value`, together with test ## Blocks Verification The Ledger MUST certify the last block (tip) recorded. The Ledger MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): -1. `last_block_index`: the index of the last block in the chain. The values MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) +1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain Clients SHOULD download the tip certificate first and then download the blocks backward starting from `last_block_index` and validate the blocks in the process. @@ -126,8 +126,11 @@ The following principles guide the evolution and interpretation of ICRC-3 and an ### 3. Avoiding Collisions in `tx` - No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. -- To avoid collisions between transactions originating from different standards, the canonical `tx` mapping MUST include: -- An operation field (`op`) whose value is namespaced using as prefix the number of the standard that introduces the method, e.g., `122freeze_account`. +- To avoid collisions across standards, the canonical `tx` mapping MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix, e.g., `122freeze_account`. + +- No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. +- To avoid collisions across standards, `tx` MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix (e.g., `122freeze_account`). This namespacing requirement applies to typed blocks; legacy ICRC-1/2 blocks keep their historical `op` values (e.g., `"xfer"`, `"mint"`, `"burn"`). + ### 4. Inclusion of the User Call in `tx` - The `tx` field must faithfully capture the structure of the user call that triggered the block. @@ -162,15 +165,16 @@ To ensure consistency across standards and implementations, the semantics of any • Example: debit/credit balances, mint, burn, update allowance. 4. Apply fee (if applicable) - • If the block type involves fees, determine the **effective fee** following ICRC-107. + • If the block type involves fees, determine the **effective fee** according to the rules defined for that block type. • Deduct the fee from the account designated as the **fee payer** for this block type. - • Adjust balances accordingly (e.g., for mints: `to` receives `amt – fee`). + • Adjust balances accordingly (e.g., for mints: `to` receives `amt – fee`). + • The destination or handling of the fee (burn, treasury, etc.) may be specified by the block type or by a separate fee standard (e.g., ICRC-107). When unspecified, the destination/handling of the fee is ledger-defined; ledgers may burn fees or route them to a treasury. See ICRC-107 for a standardized way to expose fee handling. + 5. Enforce validity conditions - • Ensure balances remain non-negative. - • Verify sufficient funds to cover `amt + fee` (where applicable). - • Require `fee ≤ amt` for mint blocks. - • Enforce any invariants specified by the block type’s standard. + • Validate that all preconditions and invariants defined by the block type’s standard are satisfied. + • This includes checks such as sufficient balances, allowance coverage, or limits on fees, as applicable. + ## Interaction with Other Standards @@ -184,7 +188,9 @@ A standard that defines a new block type MUST: - Assign a unique `btype`. - Specify the minimal `tx` structure required to interpret the block and determine its effect on ledger state. - Define semantics using the **Semantics of Blocks: Evaluation Model** (pre-fee transition, fee hook, post-conditions). -- If the block type involves fees, reference the applicable fee standard (e.g., ICRC-107) and **define who pays**, via a fee payer expression resolvable from block fields. +- If the block type involves fees, clarify what the **effective fee** is (i.e., the fee that is actually charged) and **define who pays**, via a fee payer expression resolvable from block fields. +- Optionally reference the applicable fee standard (e.g., ICRC-107) to specify **where the fee goes** (burn, treasury, etc.). + ### Standards That Define Methods A standard that defines a method which produces blocks MUST: @@ -205,16 +211,15 @@ To avoid collisions across standards, `tx.op` MUST be namespaced: - `op = icrc_number op_name` - `icrc_number`: a non-zero digit followed by zero or more digits - `op_name`: starts with a lowercase letter, then lowercase letters, digits, `_` or `-` -**Examples:** `1transfer`, `2transfer_from`, `123freeze_account`. - - +**Examples:** `1transfer`, `2transfer_from`, `123freeze_account`. +Legacy ICRC-1/2 blocks are not retrofitted with namespaced `op` values; they retain their historical operation names (e.g., `"xfer"`, `"mint"`, `"burn"`). ### Note on Fees -ICRC-3 itself does not define fee semantics. -Standards that define block types which involve fees MUST follow **ICRC-107 (Fee Handling in Blocks)**. -ICRC-3 only requires that the fee payer for a block type be clearly defined, so that fee responsibility is unambiguous. +ICRC-3 standardizes how fees are recorded in blocks, but it does not prescribe how fees are calculated or collected. +Every standard that introduces a block type involving fees MUST specify who the fee payer is so that responsibility is unambiguous. +The rules for interpreting the amount and destination of fees are defined in ICRC-107 (Fee Handling in Blocks). Ledgers that do not yet implement ICRC-107 MAY still produce valid ICRC-3 blocks, but their fee behavior will be ledger-specific until aligned with ICRC-107. ## Supported Standards @@ -250,6 +255,24 @@ A legacy block: - **MAY** include: - `"fee": Nat` — the fee actually charged by the ledger, if any. + + + +### Effective Fee + +The **effective fee** is the fee charged by the ledger. For a block, this is computed as: + +1. If a top-level `"fee"` is present, then `effective_fee = fee`. +2. Otherwise, if `tx.fee` is present, then `effective_fee = tx.fee`. +3. Otherwise, `effective_fee = 0`. + +- `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. +- If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. +- Ledgers **MAY** omit the top-level `"fee"` when it equals `tx.fee` to save space. +- What happens with the effective fee (e.g., burning it, sending it to a collector account, redistributing) is up to the ledger implementation. A common policy is to burn fees. +- **ICRC-107** specifies how fee collection and handling are formalized. Ledgers that wish to expose their fee policy in a standardized way should follow that specification. + + --- #### Transfer Block (`op = "xfer"`) @@ -266,9 +289,9 @@ A legacy block: **Semantics** Transfers debit `amt` (and any fee) from `from` and credit `amt` to `to`. -If `tx.spender` is present, the operation is executed under an approval, which must cover at least `amt + fee`. The allowance is reduced accordingly. +If `tx.spender` is present, the operation is executed under an approval, which must cover at least `tx.amt + effective_fee`. The allowance is reduced accordingly. -**Fee payer:** `from`. +**Fee payer:** `tx.from`. --- @@ -279,17 +302,17 @@ If `tx.spender` is present, the operation is executed under an approval, which m - **MUST** contain `tx.to : Account`. - **MUST** contain `tx.amt : Nat`. - **MUST NOT** contain `tx.from`. +- **MUST NOT** contain `tx.fee`. - **MAY** contain `tx.spender : Account`. - **MAY** contain `tx.memo : Blob` if provided by the caller. - **MAY** contain `tx.ts : Nat` if provided by the caller. -**Semantics** -Mints create `amt` new tokens. If a fee is charged, it is deducted from `to` immediately, so `to` receives `amt - fee` (require `fee ≤ amt`). -If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `amt + fee` and **MUST** be reduced by `amt + fee`. +Mints create `tx.amt` new tokens. If an effective fee is charged, it is deducted from `tx.to` immediately, so `tx.to` receives `tx.amt - effective_fee` (require `effective_fee ≤ tx.amt`). +If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `tx.amt + effective_fee` and **MUST** be reduced by `tx.amt + effective_fee`. -**Fee payer:** `to`. +**Fee payer:** `tx.to`. --- @@ -305,9 +328,9 @@ If `tx.spender` is present, the mint is executed under an approval on the mintin - **MAY** contain `tx.ts : Nat` if provided by the caller. **Semantics** -Burns remove `amt` tokens from `from`. Any fee is also debited from `from`. +Burns remove `tx.amt` tokens from `tx.from`. Any fee is also debited from `tx.from`. -**Fee payer:** `from`. +**Fee payer:** `tx.from`. --- @@ -323,27 +346,16 @@ Burns remove `amt` tokens from `from`. Any fee is also debited from `from`. - **MAY** contain `tx.ts : Nat` if provided by the caller. **Semantics** -Approvals set or update the allowance of `spender` on `from`. +Approvals set or update the allowance of `tx.spender` on `tx.from`. Any subsequent `xfer` block with `tx.spender` consumes the allowance. -Fees (if any) are debited from `from`. -If the approval is set on the minting account, it can be consumed by `icrc2_transfer_from` mints; such mints reduce the allowance by `amt + fee`. +Fees (if any) are debited from `tx.from`. +If the approval is set on the minting account, it can be consumed by `icrc2_transfer_from` mints; such mints reduce the allowance by `tx.amt + effective_fee`. -**Fee payer:** `from`. +**Fee payer:** `tx.from`. --- -#### Notes on Fee Representation (Legacy Blocks) - -- The **effective fee** is the fee charged by the ledger. For a block this is computed as: - 1. If a top-level `"fee"` is present, that is the effective fee. - 2. Otherwise, if `tx.fee` is present, the effective fee equals `tx.fee`. - 3. Otherwise, the effective fee is `0`. - -- `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. -- If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. -- Ledgers **MAY** omit the top-level `"fee"` when it equals `tx.fee` to save space. -- The **destination/handling** of the fee (e.g., collecting account, burn) is specified by the fee standard (see **ICRC-107, Fee Handling in Blocks**); ICRC-3 only standardizes how fees are recorded in blocks, not where they go. @@ -354,7 +366,8 @@ Although legacy ICRC-1 and ICRC-2 blocks do not include the `btype` field, ledge - `"1burn"` for burn blocks - `"1mint"` for mint blocks - `"1xfer"` for `icrc1_transfer` blocks -- `"2xfer"` for `icrc2_transfer_from` blocks +- `"2xfer"` for `icrc2_transfer_from` transfer blocks +- `"2mint"` for `icrc2_transfer_from` delegated mint blocks - `"2approve"` for `icrc2_approve` blocks @@ -393,7 +406,7 @@ All fields are encoded using the ICRC-3 `Value` type. **Call parameters:** -```candid +``` icrc1_transfer: record { to: Account; amount: Nat; @@ -440,6 +453,7 @@ icrc1_transfer: record { + ### Canonical Examples of `icrc1_transfer` Blocks Each of the following examples represents a canonical block resulting from an `icrc1_transfer` call. These examples illustrate different scenarios depending on which optional fields were included in the call. Only parameters explicitly provided by the caller appear in the resulting `tx`. @@ -714,7 +728,7 @@ variant { type Value = variant { Blob : blob; Text : text; - Nat : nat; // do we need this or can we just use Int? + Nat : nat; Int : int; Array : vec Value; Map : vec record { text; Value }; From f4fd592bcb56e43be8ff9c3f41ecb1740a464f6a Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 18 Sep 2025 15:05:45 +0200 Subject: [PATCH 16/38] minor changes --- standards/ICRC-3/README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 52a2a6c..e8a8e38 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -125,9 +125,6 @@ The following principles guide the evolution and interpretation of ICRC-3 and an - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). ### 3. Avoiding Collisions in `tx` -- No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. -- To avoid collisions across standards, the canonical `tx` mapping MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix, e.g., `122freeze_account`. - - No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. - To avoid collisions across standards, `tx` MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix (e.g., `122freeze_account`). This namespacing requirement applies to typed blocks; legacy ICRC-1/2 blocks keep their historical `op` values (e.g., `"xfer"`, `"mint"`, `"burn"`). @@ -167,7 +164,7 @@ To ensure consistency across standards and implementations, the semantics of any 4. Apply fee (if applicable) • If the block type involves fees, determine the **effective fee** according to the rules defined for that block type. • Deduct the fee from the account designated as the **fee payer** for this block type. - • Adjust balances accordingly (e.g., for mints: `to` receives `amt – fee`). + • Adjust balances accordingly (e.g., for mints: `to` receives `amt - fee`). • The destination or handling of the fee (burn, treasury, etc.) may be specified by the block type or by a separate fee standard (e.g., ICRC-107). When unspecified, the destination/handling of the fee is ledger-defined; ledgers may burn fees or route them to a treasury. See ICRC-107 for a standardized way to expose fee handling. @@ -288,9 +285,12 @@ The **effective fee** is the fee charged by the ledger. For a block, this is com - **MAY** contain `tx.spender : Account` if created via `icrc2_transfer_from`. **Semantics** -Transfers debit `amt` (and any fee) from `from` and credit `amt` to `to`. +Transfers debit `tx.amt` (and any fee) from `tx.from` and credit `tx.amt` to `tx.to`. If `tx.spender` is present, the operation is executed under an approval, which must cover at least `tx.amt + effective_fee`. The allowance is reduced accordingly. +**Minting-account prohibition.** A block with `tx.op = "xfer"` MUST NOT have either endpoint equal to the minting account. If `tx.from` equals the minting account, the operation MUST be represented as `op = "mint"`. If `tx.to` equals the minting account, the operation MUST be represented as `op = "burn"`. It is strictly prohibited for both `tx.from` and `tx.to` to be the minting account simultaneously. + + **Fee payer:** `tx.from`. --- @@ -311,7 +311,6 @@ Mints create `tx.amt` new tokens. If an effective fee is charged, it is deducted If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `tx.amt + effective_fee` and **MUST** be reduced by `tx.amt + effective_fee`. - **Fee payer:** `tx.to`. --- @@ -323,7 +322,7 @@ If `tx.spender` is present, the mint is executed under an approval on the mintin - **MUST** contain `tx.from : Account`. - **MUST** contain `tx.amt : Nat`. - **MUST NOT** contain `tx.to`. -- **MAY** contain `tx.fee : Nat` if provided by the caller. +- **MUST NOT** contain `tx.fee`. - **MAY** contain `tx.memo : Blob` if provided by the caller. - **MAY** contain `tx.ts : Nat` if provided by the caller. @@ -363,12 +362,13 @@ If the approval is set on the minting account, it can be consumed by `icrc2_tran Although legacy ICRC-1 and ICRC-2 blocks do not include the `btype` field, ledgers **MUST** still report their supported block types via the `icrc3_supported_block_types` endpoint. By convention, the following identifiers are used to describe the types of these legacy blocks: -- `"1burn"` for burn blocks -- `"1mint"` for mint blocks -- `"1xfer"` for `icrc1_transfer` blocks -- `"2xfer"` for `icrc2_transfer_from` transfer blocks -- `"2mint"` for `icrc2_transfer_from` delegated mint blocks -- `"2approve"` for `icrc2_approve` blocks +- "1burn" for burn blocks +- "1mint" for mint blocks +- "1xfer" for `icrc1_transfer` blocks +- "2xfer" for `icrc2_transfer_from` transfer blocks +- "2burn" for `icrc2_transfer_from` burn blocks +- "2mint" for `icrc2_transfer_from` delegated mint blocks +- "2approve" for `icrc2_approve` blocks ### Account Type @@ -451,6 +451,7 @@ icrc1_transfer: record { - `to` and `fee` MUST NOT be present +**Invalid combination (rejected).** If both the sender and the recipient resolve to the minting account in the same call, the ledger MUST reject the call; no legacy `xfer` block is produced for this case. @@ -604,7 +605,6 @@ variant { - `spender = [caller]` if `spender_subaccount` is not provided - `spender = [caller, spender_subaccount]` if provided - `amt = amount` -- `fee = fee` if provided - `memo = memo` if provided - `ts = created_at_time` if provided @@ -620,6 +620,14 @@ variant { - `ts = created_at_time` if provided - `from` and `fee` **MUST NOT** be present +**Invalid combination (rejected).** If both `from` and `to` are the minting account in the same `icrc2_transfer_from` call, the ledger MUST reject the call; no `xfer`/`burn`/`mint` block is produced for this case. + + + +### Canonical Examples of `icrc2_transfer_from` Blocks + +Each of the following examples represents a canonical block resulting from an `icrc2_transfer_from` call. These examples illustrate different scenarios depending on which optional fields were included in the call. Only parameters explicitly provided by the caller appear in the resulting `tx`. + #### Example 4: Transfer from approval This example shows an `icrc2_transfer_from` call where the recipient is a regular user account. Only the required fields are provided: `from`, `to`, and `amount`, and the spender subaccount is omitted (defaults to `null`, i.e., the default subaccount). From 35f2c3ffd1a1833a66be5299ba0b483775cc1e6d Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 18 Sep 2025 15:37:19 +0200 Subject: [PATCH 17/38] q-int's comment & fix transfer from based mints semantic --- standards/ICRC-3/README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index e8a8e38..652c5da 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -308,7 +308,7 @@ If `tx.spender` is present, the operation is executed under an approval, which m - **MAY** contain `tx.ts : Nat` if provided by the caller. Mints create `tx.amt` new tokens. If an effective fee is charged, it is deducted from `tx.to` immediately, so `tx.to` receives `tx.amt - effective_fee` (require `effective_fee ≤ tx.amt`). -If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `tx.amt + effective_fee` and **MUST** be reduced by `tx.amt + effective_fee`. +If `tx.spender` is present, the mint is executed under an approval on the minting account; that approval **MUST** be at least `tx.amt` and **MUST** be reduced by `tx.amt`. **Fee payer:** `tx.to`. @@ -348,7 +348,7 @@ Burns remove `tx.amt` tokens from `tx.from`. Any fee is also debited from `tx.fr Approvals set or update the allowance of `tx.spender` on `tx.from`. Any subsequent `xfer` block with `tx.spender` consumes the allowance. Fees (if any) are debited from `tx.from`. -If the approval is set on the minting account, it can be consumed by `icrc2_transfer_from` mints; such mints reduce the allowance by `tx.amt + effective_fee`. +If the approval is set on the minting account, it can be consumed by `icrc2_transfer_from` mints; such mints reduce the allowance by `tx.amt`. **Fee payer:** `tx.from`. @@ -376,21 +376,24 @@ Although legacy ICRC-1 and ICRC-2 blocks do not include the `btype` field, ledge ICRC-1 Account is represented as an `Array` containing the `owner` bytes and optionally the subaccount bytes. Two examples of accounts, one with subaccount and the second without are below. Example of account representation as an array with two blobs, one for the owner principal and the second for the subaccount: - ``` -variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; - variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; - }}; +variant { + Array = vec { + variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; + variant { Blob = blob "\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00" }; + }; +}; ``` Example of account representation as an array with one blob encoding the owner principal. ``` -variant { Array = vec { - variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; - - }}; +variant { + Array = vec { + variant { Blob = blob "\00\00\00\00\00\f0\13x\01\01" }; + }; +}; + ``` From 45a4b8e1e240650a704569bb39c29b356f12f99a Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Tue, 7 Oct 2025 16:17:41 +0200 Subject: [PATCH 18/38] make the standard about an event log --- standards/ICRC-3/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 652c5da..b66915f 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -4,7 +4,12 @@ |:------:| | [Accepted](https://dashboard.internetcomputer.org/proposal/128824) | -`ICRC-3` is a standard for accessing the block log of a Ledger on the [Internet Computer](https://internetcomputer.org). + +`ICRC-3` is a standard for exposing a **verifiable, append-only block log** on the Internet Computer [Internet Computer](https://internetcomputer.org). +While widely used by token ledgers, ICRC-3 is **domain-agnostic**: any canister that emits a +sequence of verifiable events (e.g., governance actions, system upgrades, oracle attestations) +can use ICRC-3 to publish, verify, and archive those events. + `ICRC-3` specifies: 1. A way to fetch the archive nodes of a Ledger From 5d9c8d8d16c923688faa8b22215c15bdeb04cf65 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Tue, 7 Oct 2025 16:24:57 +0200 Subject: [PATCH 19/38] spelled out the generic aspect --- standards/ICRC-3/README.md | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index b66915f..5ff01f0 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -5,17 +5,41 @@ | [Accepted](https://dashboard.internetcomputer.org/proposal/128824) | -`ICRC-3` is a standard for exposing a **verifiable, append-only block log** on the Internet Computer [Internet Computer](https://internetcomputer.org). +`ICRC-3` is a standard for exposing a **verifiable, append-only block log** on the [Internet Computer](https://internetcomputer.org). While widely used by token ledgers, ICRC-3 is **domain-agnostic**: any canister that emits a sequence of verifiable events (e.g., governance actions, system upgrades, oracle attestations) can use ICRC-3 to publish, verify, and archive those events. -`ICRC-3` specifies: -1. A way to fetch the archive nodes of a Ledger -2. A generic format for sharing the block log without information loss. This includes the fields that a block must have -3. A mechanism to verify the block log on the client side to allow downloading the block log via query calls -4. A way for new standards to define new transaction types compatible with ICRC-3 +## Scope & Non-Goals + +**Scope.** ICRC-3 standardizes: +- A canonical, representation-independent `Value` type to encode blocks losslessly. +- A linked block structure (`phash` → parent hash) enabling client-side verification. +- Endpoints for retrieving recent and archived blocks, and for verifying the tip. +- A mechanism (`btype`) for higher-level standards to define event semantics. + +**Non-Goals.** +- ICRC-3 does **not** prescribe economic semantics (fees, rewards, slashing, etc.). +- ICRC-3 does **not** define what an event “means.” Semantics are defined by the block’s `btype` + and the standard or application that introduces it. + + +## What ICRC-3 Specifies + +ICRC-3 defines the **structure, linkage, and access model** for verifiable event logs on the +Internet Computer. It standardizes: + +1. A way for clients to fetch information about archive canisters associated with the log. +2. A generic, lossless format for representing blocks and their contents using the canonical `Value` type. +3. A mechanism for verifying block log integrity on the client side, enabling query-based block retrieval. +4. A way for new standards and applications to define **domain-specific block types** (`btype`) that remain interoperable under the same verifiable log framework. + +ICRC-3 does **not** prescribe the semantics of these blocks—it only defines how they are encoded, +linked, and verified. Higher-level standards such as ICRC-1, ICRC-2, or ICRC-107 use ICRC-3 +to record domain-specific state transitions (e.g., transfers, approvals, or fees), while +other systems may use it to log governance actions, oracle attestations, or protocol events. + ## Archive Nodes From 625110708528960c987d930283220ed9d6fdcf5f Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Wed, 8 Oct 2025 16:19:11 +0200 Subject: [PATCH 20/38] language ledger agnonstic --- standards/ICRC-3/README.md | 65 +++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 5ff01f0..316634d 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -16,8 +16,8 @@ can use ICRC-3 to publish, verify, and archive those events. **Scope.** ICRC-3 standardizes: - A canonical, representation-independent `Value` type to encode blocks losslessly. - A linked block structure (`phash` → parent hash) enabling client-side verification. -- Endpoints for retrieving recent and archived blocks, and for verifying the tip. -- A mechanism (`btype`) for higher-level standards to define event semantics. +- Endpoints for retrieving recent and archived blocks, and for verifying the tip of the chain. +- A mechanism (`btype`) that allows higher-level standards to define event semantics. **Non-Goals.** - ICRC-3 does **not** prescribe economic semantics (fees, rewards, slashing, etc.). @@ -30,10 +30,12 @@ can use ICRC-3 to publish, verify, and archive those events. ICRC-3 defines the **structure, linkage, and access model** for verifiable event logs on the Internet Computer. It standardizes: -1. A way for clients to fetch information about archive canisters associated with the log. -2. A generic, lossless format for representing blocks and their contents using the canonical `Value` type. +1. A generic, lossless format for representing blocks and their contents using the canonical `Value` type. +2. A linked block structure (`phash` → parent hash) enabling client-side verification. 3. A mechanism for verifying block log integrity on the client side, enabling query-based block retrieval. -4. A way for new standards and applications to define **domain-specific block types** (`btype`) that remain interoperable under the same verifiable log framework. +4. A way for clients to fetch information about archive canisters associated with the log. +5. A way for new standards and applications to define **domain-specific block types** (`btype`) that remain interoperable under the same verifiable log framework. + ICRC-3 does **not** prescribe the semantics of these blocks—it only defines how they are encoded, linked, and verified. Higher-level standards such as ICRC-1, ICRC-2, or ICRC-107 use ICRC-3 @@ -41,13 +43,38 @@ to record domain-specific state transitions (e.g., transfers, approvals, or fees other systems may use it to log governance actions, oracle attestations, or protocol events. -## Archive Nodes -The Ledger must expose an endpoint `icrc3_get_archives` listing all the canisters containing its blocks. +## Terminology and Block Structure + +ICRC-3 defines blocks as **verifiable records** that together form an append-only chain. +Each block encapsulates a transaction or event in a canonical `Value` representation and +links to its predecessor through the `phash` (parent hash). + +Blocks are **domain-neutral**: the `btype` field identifies which higher-level standard or +application defines the semantics of the `tx` content. This enables interoperable verification across domains—ledgers, governance systems, or any canister that emits a sequence of verifiable actions. + +Although many deployments are token ledgers, the same verifiable log applies to governance decisions, protocol upgrades, oracle attestations, or any application that benefits from an auditable sequence of certified events. + +## Block Retrieval and Archival + +Large canisters may offload older blocks to archive canisters. +ICRC-3 defines standard endpoints for: + +- Querying the list of archive canisters (`icrc3_get_archives`), +- Retrieving blocks from both the primary and archive canisters (`icrc3_get_blocks`), +- Verifying continuity between archived and unarchived segments. + +This enables clients to reconstruct the entire verified log even when its storage +is distributed across multiple archive canisters. + +The following sections specify the generic block format, the meaning of its top-level fields, +and the rules for defining new block types under ICRC-3. + ## Block Log -The block log is a list of blocks where each block contains the hash of its parent (`phash`). The parent of a block `i` is block `i-1` for `i>0` and `null` for `i=0`. +- The parent of block `i` is block `i-1` for `i > 0`, and `null` for the genesis block (`i = 0`). + ``` ┌─────────────────────────┐ ┌─────────────────────────┐ @@ -85,7 +112,7 @@ Servers MUST serve the block log as a list of `Value` where each `Value` represe `ICRC-3` specifies a standard hash function over `Value`. -This hash function SHOULD be used by Ledgers to calculate the hash of the parent of a block and by clients to verify the downloaded block log. +This hash function MUST be used by log producer canister to calculate the hash of the parent of a block and by clients to verify the downloaded block log. The hash function is the [representation-independent hashing of structured data](https://internetcomputer.org/docs/current/references/ic-interface-spec#hash-of-map) used by the IC: - the hash of a `Blob` is the hash of the bytes themselves @@ -99,7 +126,7 @@ Pseudocode for representation-independent hashing of `Value`, together with test ## Blocks Verification -The Ledger MUST certify the last block (tip) recorded. The Ledger MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): +The log producer canister MUST certify the last block (tip) recorded. The log producer canister MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): 1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain @@ -122,7 +149,7 @@ An ICRC-3 compliant Block An ICRC-3 block can record different kinds of information. Some blocks record the result of a transaction submitted by a user. These typically contain a `tx` field describing the user’s intent and any parameters they provided. -Other blocks may be created by the ledger itself, for example during an upgrade, migration, or system operation, to record changes in ledger state that did not come from a user call. +Other blocks may be created by the log producer canister itself, for example during an upgrade, migration, or system operation, to record changes in the canistesr state that did not come from a user call. The `tx` field, when present, encodes the **intent** or **state change payload** associated with the block: - In user-initiated blocks, `tx` reflects the call parameters, subject to the canonical mapping defined for that block type. @@ -250,10 +277,10 @@ The rules for interpreting the amount and destination of fees are defined in ICR ## Supported Standards -An ICRC-3 compatible Ledger MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. +An ICRC-3 compatible log producer canister MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. -- For **typed** blocks, the ledger MUST only produce blocks whose `"btype"` value is included in this list. -- For **legacy** ICRC-1/2 blocks (no `"btype"`), the ledger MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. +- For **typed** blocks, the log producer canister MUST only produce blocks whose `"btype"` value is included in this list. +- For **legacy** ICRC-1/2 blocks (no `"btype"`), the log producer canister MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema @@ -276,10 +303,10 @@ A legacy block: - **MUST** be a `Value::Map` containing at least: - `"phash"`: `Blob` — the parent hash. - - `"ts"`: `Nat` — the timestamp (in nanoseconds since Unix epoch) set by the ledger when the block was created. + - `"ts"`: `Nat` — the timestamp (in nanoseconds since Unix epoch) set by the log producer canister when the block was created. - `"tx"`: `Value::Map` — representing the user’s transaction intent. - **MAY** include: - - `"fee": Nat` — the fee actually charged by the ledger, if any. + - `"fee": Nat` — the fee actually charged by the log producer canister, if any. @@ -776,7 +803,7 @@ type Value = variant { type GetArchivesArgs = record { // The last archive seen by the client. - // The Ledger will return archives coming + // The log producer will return archives coming // after this one if set, otherwise it // will return the first archives. from : opt principal; @@ -799,11 +826,11 @@ type GetBlocksResult = record { // Total number of blocks in the block log log_length : nat; - // Blocks found locally to the Ledger + // Blocks found locally to the producing canister blocks : vec record { id : nat; block: Value }; // List of callbacks to fetch the blocks that are not local - // to the Ledger, i.e. archived blocks + // to the producing canister, i.e. archived blocks archived_blocks : vec record { args : GetBlocksArgs; callback : func (GetBlocksArgs) -> (GetBlocksResult) query; From f4a52118bdedd3149d855433ced2ca7bf81868fe Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 9 Oct 2025 10:33:16 +0200 Subject: [PATCH 21/38] relaxed core state transition --- standards/ICRC-3/README.md | 49 ++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 316634d..8ce9352 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -55,25 +55,13 @@ application defines the semantics of the `tx` content. This enables interoperab Although many deployments are token ledgers, the same verifiable log applies to governance decisions, protocol upgrades, oracle attestations, or any application that benefits from an auditable sequence of certified events. -## Block Retrieval and Archival - -Large canisters may offload older blocks to archive canisters. -ICRC-3 defines standard endpoints for: - -- Querying the list of archive canisters (`icrc3_get_archives`), -- Retrieving blocks from both the primary and archive canisters (`icrc3_get_blocks`), -- Verifying continuity between archived and unarchived segments. - -This enables clients to reconstruct the entire verified log even when its storage -is distributed across multiple archive canisters. - -The following sections specify the generic block format, the meaning of its top-level fields, -and the rules for defining new block types under ICRC-3. - ## Block Log - The parent of block `i` is block `i-1` for `i > 0`, and `null` for the genesis block (`i = 0`). ++ The parent of block `i` is block `i-1` for `i > 0`. ++ The genesis block (`i = 0`) has no parent and therefore MUST NOT include a `"phash"` field. + ``` @@ -86,6 +74,22 @@ and the rules for defining new block types under ICRC-3. ``` +## Block Retrieval and Archival + +Large canisters may offload older blocks to archive canisters. +ICRC-3 defines standard endpoints for: + +- Querying the list of archive canisters (`icrc3_get_archives`), +- Retrieving blocks from both the primary and archive canisters (`icrc3_get_blocks`), +- Verifying continuity between archived and unarchived segments. + +This enables clients to reconstruct the entire verified log even when its storage +is distributed across multiple archive canisters. + +The following sections specify the generic block format, the meaning of its top-level fields, +and the rules for defining new block types under ICRC-3. + + ## Value The [candid](https://github.com/dfinity/candid) format supports sharing information even when the client and the server involved do not have the same schema (see the [Upgrading and subtyping](https://github.com/dfinity/candid/blob/master/spec/Candid.md#upgrading-and-subtyping) section of the candid spec). While this mechanism allows to evolve services and clients @@ -160,14 +164,19 @@ Block types and their schemas are defined either by legacy standards (e.g., ICRC -## Principles and Rules for ICRC-3 Blocks +## Principles for ICRC-3 Blocks The following principles guide the evolution and interpretation of ICRC-3 and any standards that build on it. -### 1. Core State Transitions -- Every block type MUST define the **core state transition** it represents: the deterministic change to ledger state implied by the block’s minimal `tx` structure, *ignoring fees or ledger-specific policies*. -- This transition is the canonical meaning of a block — what balances, allowances, or other state variables change as a direct consequence of the block. -- Fee handling, metadata, and ledger-specific policies are layered on top of this transition. +### 1. Deterministic Meaning + +- Every block type SHOULD define the deterministic change or effect it represents in the system’s state. + +- This description captures the intended meaning of a block — what observable change or event it records — without prescribing how that change is implemented. + +- For example, in ledgers this may refer to a balance update; in other domains, it could mean a configuration change, governance vote, or emitted signal. + +- Fee handling or application-specific policies are layered on top of this meaning and may be standardized separately. ### 2. Separation of `btype` and `tx` - The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its core state transition. From 06ae958b4a48d9513187204274c53014316e07dc Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 9 Oct 2025 10:49:02 +0200 Subject: [PATCH 22/38] relaxed the btype/tx recommmendations --- standards/ICRC-3/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 8ce9352..dbff65c 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -178,12 +178,15 @@ The following principles guide the evolution and interpretation of ICRC-3 and an - Fee handling or application-specific policies are layered on top of this meaning and may be standardized separately. + ### 2. Separation of `btype` and `tx` -- The `btype` field defines the **minimal semantic structure** of a block — the set of fields in `tx` required to fully determine its core state transition. +- The btype field identifies the semantic family of a block — which higher-level standard or application defines its interpretation. + - Standards that introduce a new `btype` MUST: - Assign a unique identifier for the `btype`. - - Specify the minimal `tx` structure required for interpreting that block type. - - Define the block’s **core state transition** in terms of this minimal structure. + - Specify the minimal structure required for interpreting that block type (including any specific fields required in `tx`). + - Define the intended meaning or effect in terms of this minimal structure. + - Standards that define methods producing blocks MUST: - Specify which `btype` the method produces. - Define the **canonical mapping** from method call parameters to the `tx` field of the resulting block. From 1f12fa2846f29275084bfaed64a1e83723a3d87f Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 9 Oct 2025 10:51:49 +0200 Subject: [PATCH 23/38] explicit demand that tx captures the user's call --- standards/ICRC-3/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index dbff65c..2385d25 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -197,10 +197,12 @@ The following principles guide the evolution and interpretation of ICRC-3 and an - To avoid collisions across standards, `tx` MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix (e.g., `122freeze_account`). This namespacing requirement applies to typed blocks; legacy ICRC-1/2 blocks keep their historical `op` values (e.g., `"xfer"`, `"mint"`, `"burn"`). -### 4. Inclusion of the User Call in `tx` -- The `tx` field must faithfully capture the structure of the user call that triggered the block. -- All call parameters that are part of the method’s canonical mapping MUST be included exactly as provided by the caller. -- Optional parameters that were not present in the call MUST be omitted from `tx`. +### 4. Capturing the User Call + +- The `tx` field **SHOULD** capture the structure of the user call or event that triggered the block. +- All call parameters that are part of the canonical mapping **MUST** be included exactly as provided. +- Optional parameters that were not present in the call **MUST NOT** appear in `tx`. + ### 5. Future-Proofing and Extensibility - Additional non-semantic fields (e.g., metadata, hashes, references) MAY be added to `tx` without introducing a new `btype`, provided: From 85bf500deb391ad6f5f04592a6718b9a9f3ef893 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 9 Oct 2025 11:40:48 +0200 Subject: [PATCH 24/38] use producer canister throughout --- standards/ICRC-3/README.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 2385d25..9ecbd63 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -8,7 +8,10 @@ `ICRC-3` is a standard for exposing a **verifiable, append-only block log** on the [Internet Computer](https://internetcomputer.org). While widely used by token ledgers, ICRC-3 is **domain-agnostic**: any canister that emits a sequence of verifiable events (e.g., governance actions, system upgrades, oracle attestations) -can use ICRC-3 to publish, verify, and archive those events. +can use ICRC-3 to publish, verify, and archive those events. + +**Producer canister.** The canister that exposes an ICRC-3 log. It may be a token ledger or any other application (e.g., governance, oracle, system services). Throughout this document, “producer canister” replaces terms like “ledger” or "producer” to emphasize that ICRC-3 is domain-agnostic. + ## Scope & Non-Goals @@ -116,7 +119,7 @@ Servers MUST serve the block log as a list of `Value` where each `Value` represe `ICRC-3` specifies a standard hash function over `Value`. -This hash function MUST be used by log producer canister to calculate the hash of the parent of a block and by clients to verify the downloaded block log. +This hash function MUST be used by producer canister to calculate the hash of the parent of a block and by clients to verify the downloaded block log. The hash function is the [representation-independent hashing of structured data](https://internetcomputer.org/docs/current/references/ic-interface-spec#hash-of-map) used by the IC: - the hash of a `Blob` is the hash of the bytes themselves @@ -130,7 +133,7 @@ Pseudocode for representation-independent hashing of `Value`, together with test ## Blocks Verification -The log producer canister MUST certify the last block (tip) recorded. The log producer canister MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): +The producer canister MUST certify the last block (tip) recorded. The producer canister MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): 1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain @@ -153,7 +156,7 @@ An ICRC-3 compliant Block An ICRC-3 block can record different kinds of information. Some blocks record the result of a transaction submitted by a user. These typically contain a `tx` field describing the user’s intent and any parameters they provided. -Other blocks may be created by the log producer canister itself, for example during an upgrade, migration, or system operation, to record changes in the canistesr state that did not come from a user call. +Other blocks may be created by the producer canister itself, for example during an upgrade, migration, or system operation, to record changes in the canistesr state that did not come from a user call. The `tx` field, when present, encodes the **intent** or **state change payload** associated with the block: - In user-initiated blocks, `tx` reflects the call parameters, subject to the canonical mapping defined for that block type. @@ -291,10 +294,10 @@ The rules for interpreting the amount and destination of fees are defined in ICR ## Supported Standards -An ICRC-3 compatible log producer canister MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. +An ICRC-3 compatible producer canister MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. -- For **typed** blocks, the log producer canister MUST only produce blocks whose `"btype"` value is included in this list. -- For **legacy** ICRC-1/2 blocks (no `"btype"`), the log producer canister MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. +- For **typed** blocks, the producer canister MUST only produce blocks whose `"btype"` value is included in this list. +- For **legacy** ICRC-1/2 blocks (no `"btype"`), the producer canister MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema @@ -317,10 +320,10 @@ A legacy block: - **MUST** be a `Value::Map` containing at least: - `"phash"`: `Blob` — the parent hash. - - `"ts"`: `Nat` — the timestamp (in nanoseconds since Unix epoch) set by the log producer canister when the block was created. + - `"ts"`: `Nat` — the timestamp (in nanoseconds since Unix epoch) set by the producer canister when the block was created. - `"tx"`: `Value::Map` — representing the user’s transaction intent. - **MAY** include: - - `"fee": Nat` — the fee actually charged by the log producer canister, if any. + - `"fee": Nat` — the fee actually charged by the producer canister, if any. @@ -817,7 +820,7 @@ type Value = variant { type GetArchivesArgs = record { // The last archive seen by the client. - // The log producer will return archives coming + // The producer will return archives coming // after this one if set, otherwise it // will return the first archives. from : opt principal; From dbbfcb4a0efc80991d930080ddfedb1882e40262 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Mon, 13 Oct 2025 12:08:34 +0200 Subject: [PATCH 25/38] added back --- standards/ICRC-3/README.md | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 9ecbd63..aebaab2 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -62,8 +62,8 @@ Although many deployments are token ledgers, the same verifiable log applies to ## Block Log - The parent of block `i` is block `i-1` for `i > 0`, and `null` for the genesis block (`i = 0`). -+ The parent of block `i` is block `i-1` for `i > 0`. -+ The genesis block (`i = 0`) has no parent and therefore MUST NOT include a `"phash"` field. +- The parent of block `i` is block `i-1` for `i > 0`. +- The genesis block (`i = 0`) has no parent and therefore MUST NOT include a `"phash"` field. @@ -98,7 +98,7 @@ and the rules for defining new block types under ICRC-3. The [candid](https://github.com/dfinity/candid) format supports sharing information even when the client and the server involved do not have the same schema (see the [Upgrading and subtyping](https://github.com/dfinity/candid/blob/master/spec/Candid.md#upgrading-and-subtyping) section of the candid spec). While this mechanism allows to evolve services and clients independently without breaking them, it also means that a client may not receive all the information that the server is sending, e.g. in case the client schema lacks some fields that the server schema has. -This loss of information is not an option for `ICRC-3`. The client must receive the same exact data the server sent in order to verify it. Verification is done by hashing the data and checking that the result is consistent with what has been certified by the server. +Lossy decoding is unacceptable for verification. The client must receive the same exact data the server sent in order to verify it. Verification is done by hashing the data and checking that the result is consistent with what has been certified by the server. For this reason, `ICRC-3` introduces the `Value` type which never changes: @@ -133,10 +133,12 @@ Pseudocode for representation-independent hashing of `Value`, together with test ## Blocks Verification -The producer canister MUST certify the last block (tip) recorded. The producer canister MUST allow to download the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): +The producer canister MUST certify the last block (tip) recorded. The producer canister MUST expose the certificate via the `icrc3_get_tip_certificate` endpoint. The certificate follows the [IC Specification for Certificates](https://internetcomputer.org/docs/current/references/ic-interface-spec#certification). The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labeled values (leaves): 1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain +These labels are direct children at the tree root (no extra path segments). + Clients SHOULD download the tip certificate first and then download the blocks backward starting from `last_block_index` and validate the blocks in the process. Validation of block `i` is done by checking the block hash against @@ -145,18 +147,19 @@ Validation of block `i` is done by checking the block hash against ## Generic Block Schema -An ICRC-3 compliant Block +An ICRC-3 compliant block: -1. MUST be a `Value` of variant `Map` -2. MUST contain a field `phash: Blob` which is the hash of its parent if it has a parent block -3. SHOULD contain a field `btype: Text` which uniquely describes the type of the Block. If this field is not set then the block type falls back to ICRC-1 and ICRC-2 for backward compatibility purposes +1. MUST be a `Value` of variant `Map`. +2. MUST contain `"phash" : Blob` — the hash of its parent block (absent only for the genesis block). +3. MUST contain `"ts" : Nat` — the producer-assigned timestamp in nanoseconds since Unix epoch. +4. SHOULD contain `"btype" : Text` — a unique identifier for the block type. If absent, the block is interpreted using the legacy ICRC-1/2 rules (see below). ### Kinds of Blocks An ICRC-3 block can record different kinds of information. Some blocks record the result of a transaction submitted by a user. These typically contain a `tx` field describing the user’s intent and any parameters they provided. -Other blocks may be created by the producer canister itself, for example during an upgrade, migration, or system operation, to record changes in the canistesr state that did not come from a user call. +Other blocks may be created by the producer canister itself, for example during an upgrade, migration, or system operation, to record changes in the canister's state that did not come from a user call. The `tx` field, when present, encodes the **intent** or **state change payload** associated with the block: - In user-initiated blocks, `tx` reflects the call parameters, subject to the canonical mapping defined for that block type. @@ -209,9 +212,10 @@ The following principles guide the evolution and interpretation of ICRC-3 and an ### 5. Future-Proofing and Extensibility - Additional non-semantic fields (e.g., metadata, hashes, references) MAY be added to `tx` without introducing a new `btype`, provided: - - They do not affect the block’s **core state transition**. + - They do not affect the block’s interpreted effects. - They are ignored by block verification and interpretation logic that only relies on the minimal `tx` structure defined by the `btype`. -- Any change to the minimal semantic structure of a block REQUIRES introducing a new `btype`. + - Any change to the minimal semantic structure of a block REQUIRES introducing a new `btype`. If additions change how a block’s effect is determined from `tx`, a new btype SHOULD be introduced. + ### Note on Ledger-Specific Fields - Blocks may include additional fields specific to a given standard or ledger (e.g., `fee`, metadata, references). @@ -446,7 +450,11 @@ Although legacy ICRC-1 and ICRC-2 blocks do not include the `btype` field, ledge ### Account Type -ICRC-1 Account is represented as an `Array` containing the `owner` bytes and optionally the subaccount bytes. Two examples of accounts, one with subaccount and the second without are below. +ICRC-1 Account is represented as an `Array` containing the `owner` bytes and optionally the subaccount bytes. + +* `owner` is the raw principal bytes. +* `subaccount` is a 32-byte blob when present. +* The account value is Array of 1 or 2 Blob items in order: `[owner, subaccount?]`. Example of account representation as an array with two blobs, one for the owner principal and the second for the subaccount: ``` From 460bb51050e931fca7f1a47f99673f6a1db3f3bc Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Mon, 13 Oct 2025 12:27:09 +0200 Subject: [PATCH 26/38] semantics for top level ts --- standards/ICRC-3/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index aebaab2..780a9f2 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -119,7 +119,8 @@ Servers MUST serve the block log as a list of `Value` where each `Value` represe `ICRC-3` specifies a standard hash function over `Value`. -This hash function MUST be used by producer canister to calculate the hash of the parent of a block and by clients to verify the downloaded block log. +This hash function MUST be used by the producer canister to compute parent hashes and by clients to verify downloaded blocks. + The hash function is the [representation-independent hashing of structured data](https://internetcomputer.org/docs/current/references/ic-interface-spec#hash-of-map) used by the IC: - the hash of a `Blob` is the hash of the bytes themselves @@ -137,6 +138,9 @@ The producer canister MUST certify the last block (tip) recorded. The producer c 1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain +The certified data root MUST commit exactly to `{ last_block_index, last_block_hash }` under those root labels; no additional certified keys are required by ICRC-3. + + These labels are direct children at the tree root (no extra path segments). Clients SHOULD download the tip certificate first and then download the blocks backward starting from `last_block_index` and validate the blocks in the process. @@ -145,6 +149,8 @@ Validation of block `i` is done by checking the block hash against 1. if `i + 1 < len(chain)` then the parent hash `phash` of the block `i+1` 2. otherwise the `last_block_hash` in the tip certificate. + + ## Generic Block Schema An ICRC-3 compliant block: @@ -154,6 +160,12 @@ An ICRC-3 compliant block: 3. MUST contain `"ts" : Nat` — the producer-assigned timestamp in nanoseconds since Unix epoch. 4. SHOULD contain `"btype" : Text` — a unique identifier for the block type. If absent, the block is interpreted using the legacy ICRC-1/2 rules (see below). +### Timestamp (`ts`) Semantics + +- `ts` is in nanoseconds since Unix epoch and set by the producer canister at block creation. +- `ts` MUST be non-decreasing across blocks (`ts[i] >= ts[i-1]`). If the system clock moves backward, clamp: `ts[i] = max(ts[i-1], now)`. +- When `ts[i] == ts[i-1]`, the block index is the tie-breaker for ordering. + ### Kinds of Blocks From a96fbcaed5b40d0b6559baf482d1eb6197d96663 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 16 Oct 2025 15:14:26 +0200 Subject: [PATCH 27/38] added one more example, removed a spurious line --- standards/ICRC-3/README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 780a9f2..22d5328 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -61,7 +61,6 @@ Although many deployments are token ledgers, the same verifiable log applies to ## Block Log -- The parent of block `i` is block `i-1` for `i > 0`, and `null` for the genesis block (`i = 0`). - The parent of block `i` is block `i-1` for `i > 0`. - The genesis block (`i = 0`) has no parent and therefore MUST NOT include a `"phash"` field. @@ -128,7 +127,7 @@ The hash function is the [representation-independent hashing of structured data] - the hash of a `Nat` is the hash of the [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) encoding of the number - the hash of an `Int` is the hash of the [`sleb128`](https://en.wikipedia.org/wiki/LEB128#Signed_LEB128) encoding of the number - the hash of an `Array` is the hash of the concatenation of the hashes of all the elements of the array -- the hash of a `Map` is the hash of the concatenation of all the hashed items of the map sorted lexicographically. A hashed item is the tuple composed by the hash of the key and the hash of the value. +- the hash of a `Map` is the hash of the concatenation of all the hashed items of the map sorted lexicographically by the keys. Map keys are compared by the lexicographic order of their UTF-8 bytes. A hashed item is the tuple composed by the hash of the key and the hash of the value. Pseudocode for representation-independent hashing of `Value`, together with test vectors to check compliance with the specification can be found [`here`](HASHINGVALUES.md). @@ -294,7 +293,7 @@ This division of responsibility ensures that: #### Namespacing for Operations To avoid collisions across standards, `tx.op` MUST be namespaced: -- `op = icrc_number op_name` +- `op = icrc_number op_name` - `icrc_number`: a non-zero digit followed by zero or more digits - `op_name`: starts with a lowercase letter, then lowercase letters, digits, `_` or `-` @@ -354,8 +353,8 @@ The **effective fee** is the fee charged by the ledger. For a block, this is com - `tx.fee` records what the caller supplied; when the top-level `"fee"` is absent, it also implies the ledger charged that same amount. - If both top-level `"fee"` and `tx.fee` are present and differ, the top-level `"fee"` is authoritative. -- Ledgers **MAY** omit the top-level `"fee"` when it equals `tx.fee` to save space. -- What happens with the effective fee (e.g., burning it, sending it to a collector account, redistributing) is up to the ledger implementation. A common policy is to burn fees. +- Implementations **MAY** omit the top-level `"fee"` when it equals `tx.fee` to save space. +- What happens with the effective fee (e.g., burning it, sending it to a collector account, redistributing) is up to the implementation. A common policy is to burn fees. - **ICRC-107** specifies how fee collection and handling are formalized. Ledgers that wish to expose their fee policy in a standardized way should follow that specification. @@ -821,8 +820,27 @@ variant { }; ``` +#### Example 6: Typed block (`btype = "107fee"`) +``` +variant { Map = vec { + // Block type + record { "btype"; variant { Text = "107feecol" }}; + + // Top-level tx (constructed per Canonical `tx` Mapping) + record { "tx"; variant { Map = vec { + record { "op"; variant { Text = "107set_fee_collector" }}; + record { "fee_collector"; variant { Array = vec { }}}; // [] means "burn from now on" + record { "created_at_time"; variant { Nat = 1_750_951_728_000_000_000 : nat }}; + record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\00\00\01\01" }}; + }}}; + + // Standard block metadata + record { "ts"; variant { Nat = 1_741_312_737_184_874_392 : nat }}; + record { "phash"; variant { Blob = blob "\2d\86\7f\34\c7\2d\1e\2d\00\84\10\a4\00\b0\b6\4c\3e\02\96\c9\e8\55\6f\dd\72\68\e8\df\8d\8e\8a\ee" }}; +}} +``` ## Specification From f987387919a722eeddea9d64c1fe1955d19b5569 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 16 Oct 2025 16:34:32 +0200 Subject: [PATCH 28/38] Nat64 --> Nat --- standards/ICRC-3/README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 22d5328..839c092 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -562,19 +562,19 @@ This example shows an `icrc1_transfer` call where the caller only specifies the ``` variant { Map = vec { - record { "fee"; variant { Nat64 = 10_000 : nat64 } }; + record { "fee"; variant { nat = 10_000 : nat } }; record { "phash"; variant { Blob = blob "\b8\0d\29\e5\91\60\4c\d4\60\3a\2a\7c\c5\33\14\21\27\b8\23\e9\a5\24\b7\14\43\24\4b\2d\d5\b0\86\13" }; }; - record { "ts"; variant { Nat64 = 1_753_344_727_778_561_060 : nat64 } }; + record { "ts"; variant { nat = 1_753_344_727_778_561_060 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat64 = 85_224_322_205 : nat64 } }; + record { "amt"; variant { nat = 85_224_322_205 : nat } }; record { "from"; variant { Array = vec { variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" } } } }; record { "op"; variant { Text = "xfer" } }; record { @@ -606,12 +606,12 @@ variant { Blob = blob "\c2\b1\32\6a\5e\09\0e\10\ad\be\f3\4c\ba\fd\bc\90\18\3f\38\a7\3e\73\61\cc\0a\fa\99\89\3d\6b\9e\47" }; }; - record { "ts"; variant { Nat64 = 1_753_344_737_123_456_789 : nat64 } }; + record { "ts"; variant { nat = 1_753_344_737_123_456_789 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat64 = 500_000_000 : nat64 } }; + record { "amt"; variant { nat = 500_000_000 : nat } }; record { "to"; variant { @@ -642,12 +642,12 @@ variant { Blob = blob "\7f\89\42\a5\be\4d\af\50\3b\6e\2a\8e\9c\c7\dd\f1\c9\e8\24\f0\98\bb\d7\af\ae\d2\90\10\67\df\1e\c1\0a" }; }; - record { "ts"; variant { Nat64 = 1_753_344_740_000_000_000 : nat64 } }; + record { "ts"; variant { nat = 1_753_344_740_000_000_000 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat64 = 42_000_000 : nat64 } }; + record { "amt"; variant { nat = 42_000_000 : nat } }; record { "from"; variant { @@ -730,19 +730,19 @@ This example shows an `icrc2_transfer_from` call where the recipient is a regula ``` variant { Map = vec { - record { "fee"; variant { Nat64 = 10_000 : nat64 } }; + record { "fee"; variant { nat = 10_000 : nat } }; record { "phash"; variant { Blob = blob "\a0\5f\d2\f3\4c\26\73\58\00\7f\ea\02\18\43\47\70\85\50\2e\d2\1f\23\e0\dc\e6\af\3c\cf\9e\6f\4a\d8" }; }; - record { "ts"; variant { Nat64 = 1_753_344_728_820_625_931 : nat64 } }; + record { "ts"; variant { nat = 1_753_344_728_820_625_931 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat64 = 50_419_165_435 : nat64 } }; + record { "amt"; variant { nat = 50_419_165_435 : nat } }; record { "from"; variant { @@ -789,12 +789,12 @@ variant { Blob = blob "\9a\cd\20\3f\b0\11\fb\7f\e2\2a\1d\f2\c1\dd\22\6a\2f\1e\f6\88\d3\b0\9f\be\8d\2e\c5\70\f2\b4\a1\77" }; }; - record { "ts"; variant { Nat64 = 1_753_344_750_000_000_000 : nat64 } }; + record { "ts"; variant { nat = 1_753_344_750_000_000_000 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { Nat64 = 200_000 : nat64 } }; + record { "amt"; variant { nat = 200_000 : nat } }; record { "from"; variant { From a495e4f83bd4a0b2c030e18794873a469426be31 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 16 Oct 2025 17:12:08 +0200 Subject: [PATCH 29/38] non-normative section with encodings of bools and optionals --- standards/ICRC-3/README.md | 39 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 839c092..99f753f 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -114,6 +114,45 @@ type Value = variant { Servers MUST serve the block log as a list of `Value` where each `Value` represents a single block in the block log. +## Common Encodings (Non-Normative) + +The `Value` type is intentionally minimal and does not include specialized variants for principals, booleans, or optional values. +This section provides **non-normative guidance** for representing such values in a consistent and interoperable way. + +### Principals +Encode principals as raw bytes using `Blob`: + +``` +variant { Blob = blob "" } +``` + +### Booleans +Encode booleans as natural numbers: +- `false` → `variant { Nat = 0 : nat }` +- `true` → `variant { Nat = 1 : nat }` + +### Optionals +Two recommended patterns: + +1. **Preferred (map-style)** — when fields can be optional, use maps and omit absent fields: + +``` +variant { Map = vec { + record { "field_a"; }; + // "field_b" omitted if absent +} } +``` + +2. **Tuple/array form** — if array/tuple positional semantics must be preserved, use a **tagged option per slot**: + - `None` → `Array [ Nat 0 ]` + - `Some(x)` → `Array [ Nat 1, x ]` + +### Rationale +These conventions avoid expanding the `Value` type while enabling consistent encoding of common structures across domains. +They are **non-binding** but strongly recommended for interoperability. + + + ## Value Hash `ICRC-3` specifies a standard hash function over `Value`. From 71bda9d45e77d48bfd4ca8a6866fbce615fbd033 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Wed, 19 Nov 2025 16:21:57 +0100 Subject: [PATCH 30/38] draft methods section --- standards/ICRC-3/README.md | 228 ++++++++++++++++++++++++------------- 1 file changed, 147 insertions(+), 81 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 99f753f..a4fc520 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -300,6 +300,143 @@ To ensure consistency across standards and implementations, the semantics of any • This includes checks such as sufficient balances, allowance coverage, or limits on fees, as applicable. +## Methods + +This section defines the methods that an ICRC-3 compliant producer canister MUST expose. +These methods provide read-only access to the certified, append-only block log described above. + +All methods in this section MUST be implemented as query methods. + + +### `icrc3_get_archives` +Returns metadata about the archive canisters that store older portions of the block log. +Clients use `from` to request only archives that appear after a previously seen archive. + +``` +type GetArchivesArgs = record { + // The last archive seen by the client. + // The producer will return archives coming + // after this one if set, otherwise it + // will return the first archives. + from : opt principal; +}; + +type GetArchivesResult = vec record { + // The id of the archive + canister_id : principal; + + // The first block in the archive + start : nat; + + // The last block in the archive + end : nat; +}; + +service : { + icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; +}; + +``` +#### Rules + +- Archives MUST be returned in strictly increasing order of their start index. +- If `from` is `null`, the producer MUST return the first archive(s). +- If from is set, the producer MUST return archives whose IDs appear after the principal `from` with that principal in the producer’s archive ordering. +- For every archive returned: + - `start` and `end` MUST describe a contiguous, inclusive block range; + - that archive MUST be able to serve exactly that range via its callbacks (as returned indirectly in `icrc3_get_blocks`). +- The producer MAY change its archival layout over time (e.g., due to upgrades, reallocation, compaction, or reinstallation). The values of `canister_id`, `start`, and `end` MUST correctly reflect the archival layout at the time of the call. +- A producer MUST NOT return overlapping or out-of-order ranges across the union of all archives. + +### `icrc3_get_blocks` +Returns a contiguous range of blocks starting at index start and spanning up to length blocks from the canonical block log. Blocks that are no longer stored locally by the producer are returned indirectly via archive callbacks. + +``` +type Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Int : int; + Array : vec Value; + Map : vec record { text; Value }; +}; + + +type GetBlocksArgs = vec record { start : nat; length : nat }; + +type GetBlocksResult = record { + // Total number of blocks in the block log + log_length : nat; + + // Blocks found locally to the producing canister + blocks : vec record { id : nat; block: Value }; + + // List of callbacks to fetch the blocks that are not local + // to the producing canister, i.e. archived blocks + archived_blocks : vec record { + args : GetBlocksArgs; + callback : func (GetBlocksArgs) -> (GetBlocksResult) query; + }; +}; + +service : { + icrc3_get_blocks : (GetBlocksArgs) -> (GetBlocksResult) query; +}; +``` + + +Rules + +- Block indices are zero-based. The genesis block, if present, has index 0. +- If start is greater than the last block index at the time of the call, the producer MUST return: + - blocks = [], and + - archived_blocks = []. +- The blocks vector MUST contain blocks for indices `[start, start + k)` for some `k ≤ length`, in strictly increasing index order, without gaps. +- The producer MAY return fewer than `length` blocks in blocks: + - due to security concerns, + - due to platform restrictions (i.e. bounds on message sizes), + - if it reaches the current tip of the log, or + - if some of the requested indices are archived. +- For every range of archived blocks in the requested interval, the producer MUST include an entry in `archived_blocks` with: + - `start` and `length` describing a contiguous index range, and + - callback pointing to a function that, when called with matching start and length, returns exactly the corresponding blocks in a blocks : vec block field. + +The union of: +- blocks returned directly in blocks, and +- blocks returned via all callback functions in `archived_blocks` MUST form a contiguous range of blocks covering all available indices in `[start, start + length)` that exist in the log at the time of the call. + +Implementations MUST NOT reorder, duplicate, or skip any existing block indices within the requested range. + + +### `icrc3_get_tip_certificate` +Returns the certified tip of the block log. The certificate authenticates a labeled subtree of the canister’s certified data that includes the last block index and the hash of the last block. + +``` +// See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification +type DataCertificate = record { + + // Signature of the root of the hash_tree + certificate : blob; + + // CBOR encoded hash_tree + hash_tree : blob; +}; + +service : { + icrc3_get_tip_certificate : () -> (opt DataCertificate) query; +}; +``` + +### `icrc3_supported_block_types` +Returns the set of block types (identified by their btype strings) that the producer canister may emit in its block log. + +``` +service : { + icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query; +}; +``` + + ## Interaction with Other Standards @@ -346,6 +483,8 @@ Every standard that introduces a block type involving fees MUST specify who the The rules for interpreting the amount and destination of fees are defined in ICRC-107 (Fee Handling in Blocks). Ledgers that do not yet implement ICRC-107 MAY still produce valid ICRC-3 blocks, but their fee behavior will be ledger-specific until aligned with ICRC-107. + + ## Supported Standards An ICRC-3 compatible producer canister MUST expose an endpoint listing all the supported block types via the endpoint `icrc3_supported_block_types`. @@ -354,12 +493,20 @@ An ICRC-3 compatible producer canister MUST expose an endpoint listing all the s - For **legacy** ICRC-1/2 blocks (no `"btype"`), the producer canister MUST include the conventional identifiers (e.g., `"1xfer"`, `"2approve"`) in this list to advertise support, even though the blocks themselves do not carry a `"btype"` field. + + + + + + + ## [ICRC-1](../ICRC-1/README.md) and [ICRC-2](../ICRC-2/README.md) Block Schema This section describes how ICRC-1 and ICRC-2 operations are represented in ICRC-3-compliant blocks. These blocks follow the **legacy format**, meaning they do not have a `btype` field. Instead, their type is inferred directly from the content of the `tx` field, which records the canonical mapping of the original method call. + ### Legacy ICRC-1 and ICRC-2 Block Structure ICRC-1 and ICRC-2 blocks **MUST NOT** include a `btype` field. These standards use the legacy block format where the block type is determined exclusively from the content of the `tx` field. @@ -881,84 +1028,3 @@ variant { Map = vec { }} ``` -## Specification - -### `icrc3_get_blocks` - -``` -type Value = variant { - Blob : blob; - Text : text; - Nat : nat; - Int : int; - Array : vec Value; - Map : vec record { text; Value }; -}; - -type GetArchivesArgs = record { - // The last archive seen by the client. - // The producer will return archives coming - // after this one if set, otherwise it - // will return the first archives. - from : opt principal; -}; - -type GetArchivesResult = vec record { - // The id of the archive - canister_id : principal; - - // The first block in the archive - start : nat; - - // The last block in the archive - end : nat; -}; - -type GetBlocksArgs = vec record { start : nat; length : nat }; - -type GetBlocksResult = record { - // Total number of blocks in the block log - log_length : nat; - - // Blocks found locally to the producing canister - blocks : vec record { id : nat; block: Value }; - - // List of callbacks to fetch the blocks that are not local - // to the producing canister, i.e. archived blocks - archived_blocks : vec record { - args : GetBlocksArgs; - callback : func (GetBlocksArgs) -> (GetBlocksResult) query; - }; -}; - -service : { - icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; - icrc3_get_blocks : (GetBlocksArgs) -> (GetBlocksResult) query; -}; -``` - -### `icrc3_get_tip_certificate` - -``` -// See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification -type DataCertificate = record { - - // Signature of the root of the hash_tree - certificate : blob; - - // CBOR encoded hash_tree - hash_tree : blob; -}; - -service : { - icrc3_get_tip_certificate : () -> (opt DataCertificate) query; -}; -``` - -### `icrc3_supported_block_types` - -``` -service : { - icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query; -}; -``` From abacc4721c46ee1f7509b7995b8d8f9ec8847d6f Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Thu, 20 Nov 2025 12:22:35 +0100 Subject: [PATCH 31/38] semantics for the remaining methods, small fixes --- standards/ICRC-3/README.md | 98 ++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 24 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index a4fc520..3fe87e1 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -166,7 +166,7 @@ The hash function is the [representation-independent hashing of structured data] - the hash of a `Nat` is the hash of the [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) encoding of the number - the hash of an `Int` is the hash of the [`sleb128`](https://en.wikipedia.org/wiki/LEB128#Signed_LEB128) encoding of the number - the hash of an `Array` is the hash of the concatenation of the hashes of all the elements of the array -- the hash of a `Map` is the hash of the concatenation of all the hashed items of the map sorted lexicographically by the keys. Map keys are compared by the lexicographic order of their UTF-8 bytes. A hashed item is the tuple composed by the hash of the key and the hash of the value. +- the hash of a `Map` is the hash of the concatenation of all the hashed items of the map sorted lexicographically by the keys. Map keys are compared by the lexicographic order of their UTF-8 bytes. A hashed item is the tuple composed of the hash of the key and the hash of the value. Pseudocode for representation-independent hashing of `Value`, together with test vectors to check compliance with the specification can be found [`here`](HASHINGVALUES.md). @@ -176,7 +176,10 @@ The producer canister MUST certify the last block (tip) recorded. The producer c 1. `last_block_index`: the index of the last block in the chain. The value MUST be expressed as [`leb128`](https://en.wikipedia.org/wiki/LEB128#Unsigned_LEB128) 2. `last_block_hash`: the hash of the last block in the chain -The certified data root MUST commit exactly to `{ last_block_index, last_block_hash }` under those root labels; no additional certified keys are required by ICRC-3. +The certified data root MUST commit to at least the labeled values +`last_block_index` and `last_block_hash` under those root labels. ICRC-3 does +not require any additional certified keys, but implementations MAY include +other labeled values in the certified data tree. These labels are direct children at the tree root (no extra path segments). @@ -341,7 +344,7 @@ service : { - Archives MUST be returned in strictly increasing order of their start index. - If `from` is `null`, the producer MUST return the first archive(s). -- If from is set, the producer MUST return archives whose IDs appear after the principal `from` with that principal in the producer’s archive ordering. +- If `from` is set, the producer MUST return archives whose IDs appear after the principal `from` with that principal in the producer’s archive ordering. - For every archive returned: - `start` and `end` MUST describe a contiguous, inclusive block range; - that archive MUST be able to serve exactly that range via its callbacks (as returned indirectly in `icrc3_get_blocks`). @@ -349,7 +352,8 @@ service : { - A producer MUST NOT return overlapping or out-of-order ranges across the union of all archives. ### `icrc3_get_blocks` -Returns a contiguous range of blocks starting at index start and spanning up to length blocks from the canonical block log. Blocks that are no longer stored locally by the producer are returned indirectly via archive callbacks. +Returns blocks for one or more requested ranges from the canonical block log. +Blocks that are no longer stored locally by the producer are returned indirectly via archive callbacks. ``` type Value = variant { @@ -385,27 +389,45 @@ service : { ``` -Rules +#### Rules + +- Block indices are zero-based. The genesis block, if present, has index `0`. + +- The `id` field of each returned local block MUST equal its block height + (i.e., its index in the canonical block log). + +- Each element `r` in `GetBlocksArgs` describes a **half-open** range + `[r.start, r.start + r.length)` of requested indices. + +- If **all** requested indices across all ranges are greater than the last block index at the time of the call, the producer MUST return: + - `blocks = []`, and + - `archived_blocks = []`. + +- The `blocks` vector: + - MUST contain locally stored blocks whose indices lie in at least a subset of the requested ranges, + - MUST be sorted by `id` in strictly increasing order, + - MUST NOT contain duplicates. -- Block indices are zero-based. The genesis block, if present, has index 0. -- If start is greater than the last block index at the time of the call, the producer MUST return: - - blocks = [], and - - archived_blocks = []. -- The blocks vector MUST contain blocks for indices `[start, start + k)` for some `k ≤ length`, in strictly increasing index order, without gaps. -- The producer MAY return fewer than `length` blocks in blocks: - - due to security concerns, - - due to platform restrictions (i.e. bounds on message sizes), - - if it reaches the current tip of the log, or - - if some of the requested indices are archived. -- For every range of archived blocks in the requested interval, the producer MUST include an entry in `archived_blocks` with: - - `start` and `length` describing a contiguous index range, and - - callback pointing to a function that, when called with matching start and length, returns exactly the corresponding blocks in a blocks : vec block field. +- The producer MAY return fewer local blocks than requested: + - due to security concerns, + - due to platform restrictions (e.g., message size limits), + - because it has reached the current tip of the log, + - because some of the requested indices are archived. -The union of: -- blocks returned directly in blocks, and -- blocks returned via all callback functions in `archived_blocks` MUST form a contiguous range of blocks covering all available indices in `[start, start + length)` that exist in the log at the time of the call. -Implementations MUST NOT reorder, duplicate, or skip any existing block indices within the requested range. +- Each entry in `archived_blocks`: + - MUST have `args` describing one or more contiguous ranges of archived indices, + - MUST provide a `callback` which, when called with a subset of those ranges, returns exactly the corresponding blocks (via its own `blocks` and/or `archived_blocks`). + +- For any requested index that is not returned in `blocks` but is available in the log, the producer MUST either: + - include that index in some `archived_blocks[i].args` range, or + - intentionally omit it due to resource or security constraints (e.g., message-size limits), or + - omit it because it lies beyond the current tip. + +- Implementations MUST NOT misrepresent the structure of the log: + - no returned block may have an incorrect `id`, + - blocks MUST appear in strictly increasing `id` order, + - and blocks MUST NOT be reordered or duplicated. ### `icrc3_get_tip_certificate` @@ -427,8 +449,24 @@ service : { }; ``` +#### Rules + +- If the producer canister has not yet emitted any blocks, it MAY return `null`. + +- Otherwise, the method MUST return an IC data certificate whose `hash_tree` authenticates a labeled subtree containing at least: + - `last_block_index` — the leb128-encoded index of the last block, and + - `last_block_hash` — the hash of that block. + +- Additional labeled values MAY be present in the certified data tree. + +- The certificate MUST be a valid IC data certificate as defined in the IC interface specification. In particular, its signature and delegation MUST verify against the IC root key, and the `hash_tree` MUST be consistent with the certified data used to derive `last_block_index` and `last_block_hash` at the time the certificate was created. + + ### `icrc3_supported_block_types` -Returns the set of block types (identified by their btype strings) that the producer canister may emit in its block log. +Returns the set of block types (`btype` identifiers) that the producer canister may emit in its block log. + +Producers that emit **legacy ICRC-1/ICRC-2 semantics** using the legacy untyped format MUST still include the corresponding legacy `btype` strings (e.g., `"1mint"`, `"1xfer"`, `"1burn"`, `"2approve"`, …). + ``` service : { @@ -436,6 +474,18 @@ service : { }; ``` +#### Rules + +- The returned vector MUST contain exactly one entry for each `btype` the producer canister is capable of emitting over its lifetime. + +- The vector MUST be sorted lexicographically by the UTF-8 bytes of `block_type`. + +- Producers that only emit legacy ICRC-1/ICRC-2 blocks MUST still return the legacy block types they support. + +- Producers that support new, typed block kinds defined by other ICRC standards (e.g., ICRC-107, ICRC-122/123/124) MUST include those `btype` identifiers as well. + +- The `url` field MUST point to a stable, canonical description of the standard defining that block type (e.g., the ICRC repository URL). + ## Interaction with Other Standards @@ -504,7 +554,7 @@ An ICRC-3 compatible producer canister MUST expose an endpoint listing all the s This section describes how ICRC-1 and ICRC-2 operations are represented in ICRC-3-compliant blocks. These blocks follow the **legacy format**, meaning they do not have a `btype` field. -Instead, their type is inferred directly from the content of the `tx` field, which records the canonical mapping of the original method call. +Instead, their type is inferred directly from the content of the `tx` field, specifically from the value of `tx.op`, which records the canonical mapping of the original method call. ### Legacy ICRC-1 and ICRC-2 Block Structure From ef6013d1daa95073ce4441541c75ecbdb8b98681 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 10:19:37 +0100 Subject: [PATCH 32/38] clearer gudiance on user calls, clearer language on icrc3_get_blocks guarantees --- standards/ICRC-3/README.md | 73 ++++++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 19 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 3fe87e1..96f070f 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -251,16 +251,43 @@ The following principles guide the evolution and interpretation of ICRC-3 and an - Define the **canonical mapping** from method call parameters to the `tx` field of the resulting block. - Ensure that `tx` contains only parameters explicitly provided by the caller (except where the block type definition requires otherwise). -### 3. Avoiding Collisions in `tx` -- No two standardized methods may produce `tx` values that are indistinguishable when interpreted under ICRC-3 rules. -- To avoid collisions across standards, `tx` MUST include an operation field (`op`) whose value is namespaced with the introducing standard’s number as a prefix (e.g., `122freeze_account`). This namespacing requirement applies to typed blocks; legacy ICRC-1/2 blocks keep their historical `op` values (e.g., `"xfer"`, `"mint"`, `"burn"`). +### 3. Avoiding Collisions Among User-Initiated Blocks +Not all blocks contain a `tx` field, and not all blocks with a `tx` field correspond to a user-initiated method call. +**Collision-avoidance requirements apply only to blocks that represent standardized user calls.** -### 4. Capturing the User Call +For any ICRC standard that defines a **user-callable method which produces blocks**: -- The `tx` field **SHOULD** capture the structure of the user call or event that triggered the block. -- All call parameters that are part of the canonical mapping **MUST** be included exactly as provided. -- Optional parameters that were not present in the call **MUST NOT** appear in `tx`. +- Blocks produced by that method SHOULD include a `tx` field. +- The `tx` field for such blocks SHOULD include an operation discriminator `op`. +- The `op` value SHOULD be namespaced with the standard’s ICRC number + (e.g., `122freeze_account`, `107set_fee_collector`) to avoid collisions with operations defined by other standards. +- The pair `(btype,tx.op)` MUST uniquely identify the method that produced the block. + +Typed blocks that **do not** represent method calls (e.g., upgrade markers, maintenance events, migration records, or system actions) MAY omit the `tx` field entirely and therefore MAY omit `tx.op`. + +Legacy ICRC-1/2 blocks continue to use their historical operation names (`"xfer"`, `"mint"`, `"burn"`, `"approve"`), and are exempt from namespacing requirements. + + +### 4. Capturing the User Call (Where Applicable) + +If a block represents the result of a **standardized user-initiated method call**, then: + +- The block **SHOULD** include a `tx` field. +- For user-initiated blocks, a namespaced `tx.op` **SHOULD** be included (see above). +- The structure of `tx` **MUST** follow the canonical mapping defined by the relevant standard. +- All parameters explicitly provided by the caller **MUST** appear in `tx` exactly as provided. +- Optional parameters that were not passed in the call **MUST NOT** appear in `tx`. + +However: + +- Blocks that are **not** created by user calls — such as system-generated blocks, upgrade or migration markers, or internal bookkeeping events — MAY omit the `tx` field entirely. +- Typed blocks created by system logic MAY include a `tx` field without an `op`, or MAY use a `tx` whose structure is defined solely by the block type (`btype`) specification. + +This distinction allows ICRC-3 to support both: +- canonical, audit-ready records of user calls, and +- domain-specific or system-generated events, +without over-constraining block structure or requiring the presence of `tx` or `tx.op` in every block. ### 5. Future-Proofing and Extensibility @@ -344,7 +371,8 @@ service : { - Archives MUST be returned in strictly increasing order of their start index. - If `from` is `null`, the producer MUST return the first archive(s). -- If `from` is set, the producer MUST return archives whose IDs appear after the principal `from` with that principal in the producer’s archive ordering. +- If `from` is set, the producer MUST return archives whose `canister_id` + appears after the given principal in the producer’s archive ordering. - For every archive returned: - `start` and `end` MUST describe a contiguous, inclusive block range; - that archive MUST be able to serve exactly that range via its callbacks (as returned indirectly in `icrc3_get_blocks`). @@ -399,12 +427,14 @@ service : { - Each element `r` in `GetBlocksArgs` describes a **half-open** range `[r.start, r.start + r.length)` of requested indices. -- If **all** requested indices across all ranges are greater than the last block index at the time of the call, the producer MUST return: +- If **all** requested indices across all ranges are greater than the last block + index at the time of the call, the producer MUST return: - `blocks = []`, and - `archived_blocks = []`. - The `blocks` vector: - - MUST contain locally stored blocks whose indices lie in at least a subset of the requested ranges, + - MUST contain locally stored blocks whose indices lie in at least a subset of + the requested ranges, - MUST be sorted by `id` in strictly increasing order, - MUST NOT contain duplicates. @@ -414,15 +444,18 @@ service : { - because it has reached the current tip of the log, - because some of the requested indices are archived. - - Each entry in `archived_blocks`: - - MUST have `args` describing one or more contiguous ranges of archived indices, - - MUST provide a `callback` which, when called with a subset of those ranges, returns exactly the corresponding blocks (via its own `blocks` and/or `archived_blocks`). - -- For any requested index that is not returned in `blocks` but is available in the log, the producer MUST either: - - include that index in some `archived_blocks[i].args` range, or - - intentionally omit it due to resource or security constraints (e.g., message-size limits), or - - omit it because it lies beyond the current tip. + - MUST have `args` describing one or more contiguous ranges of archived + indices, and + - MUST provide a `callback` which, when called with a subset of those ranges, + returns blocks whose `id` values lie within the requested ranges, again + subject to the same kinds of constraints (message size, security limits, + reaching that archive’s tip, etc.). + +- Implementations MAY therefore return only a **partial view** of the blocks in + the requested ranges. Clients MUST be prepared to receive fewer blocks than + actually exist in those ranges and MAY issue additional `icrc3_get_blocks` + calls with narrower or adjusted ranges if they require full coverage. - Implementations MUST NOT misrepresent the structure of the log: - no returned block may have an incorrect `id`, @@ -509,7 +542,9 @@ A standard that defines a method which produces blocks MUST: - Define the canonical mapping from method inputs to the `tx` field of the resulting block. - Ensure all required fields from the block type’s minimal schema are populated. - Include only caller-provided optional fields; omit optionals that were not supplied. -- Include an `op` field in `tx` to identify the operation and avoid collisions. +- For methods that represent user-initiated calls, include an `op` field in + `tx` (namespaced as described above) to identify the operation and avoid + collisions. This division of responsibility ensures that: - Block types define **what blocks mean** (semantics). From a974a971952d31e460d301240a3d1119278d0cf1 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 13:32:37 +0100 Subject: [PATCH 33/38] further clarifications on tx, tx.op etc --- standards/ICRC-3/README.md | 119 ++++++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 16 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 96f070f..6b35844 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -262,7 +262,6 @@ For any ICRC standard that defines a **user-callable method which produces block - The `tx` field for such blocks SHOULD include an operation discriminator `op`. - The `op` value SHOULD be namespaced with the standard’s ICRC number (e.g., `122freeze_account`, `107set_fee_collector`) to avoid collisions with operations defined by other standards. -- The pair `(btype,tx.op)` MUST uniquely identify the method that produced the block. Typed blocks that **do not** represent method calls (e.g., upgrade markers, maintenance events, migration records, or system actions) MAY omit the `tx` field entirely and therefore MAY omit `tx.op`. @@ -303,16 +302,28 @@ without over-constraining block structure or requiring the presence of `tx` or ` ## Semantics of Blocks: Evaluation Model -To ensure consistency across standards and implementations, the semantics of any block must be interpretable through the following evaluation model. Each standard that defines a block type specifies how to “plug into” this model (by defining its minimal `tx` schema, pre-fee transition, fee payer, etc.). +To ensure consistency across standards and implementations, the semantics of any +block must be interpretable through the following evaluation model. Each standard +that defines a block type specifies how to “plug into” this model (by defining +its minimal `tx` schema, pre-fee transition, fee payer, etc.). For block types +that do not use a `tx` field, the standard MUST specify how to interpret the +block directly from its top-level fields. + 1. Identify block type • If `btype` is present, use it. • If no `btype`, fall back to legacy ICRC-1/2 inference from `tx.op`. 2. Validate `tx` structure - • Check that `tx` includes all required fields defined for the block type. - • Ensure no extra *semantic* fields beyond those defined by the block type are present. - • Optional caller-provided fields may appear if allowed by the canonical mapping. + • Check that all semantic fields that the block type specification + marks as required, and that they have the expected shape. + • If the block type specification allows additional optional or + extension fields, they MAY be present. + • ICRC-3 itself does not distinguish between “semantic” and + “non-semantic” fields; it is the responsibility of the block type + specification to state which fields affect the meaning of the block + and how extra fields are to be treated (e.g., ignored by generic + interpreters). 3. Derive pre-fee state transition • Apply the deterministic state change implied by `tx`, ignoring any fees. @@ -528,13 +539,60 @@ ICRC-3 defines how blocks are structured and verified. Other standards extend th (2) defining canonical mappings from standardized method calls to existing block types. ### Standards That Introduce Block Types + +A standard may define one or more **block types** (`btype`) that represent +domain-specific events. Block-type standards define **what a block means**, not how +blocks are created. The creation of blocks (i.e., the mapping from method calls to `tx` +fields) is handled separately by the standard that defines the method. + A standard that defines a new block type MUST: -- Assign a unique `btype`. -- Specify the minimal `tx` structure required to interpret the block and determine its effect on ledger state. -- Define semantics using the **Semantics of Blocks: Evaluation Model** (pre-fee transition, fee hook, post-conditions). -- If the block type involves fees, clarify what the **effective fee** is (i.e., the fee that is actually charged) and **define who pays**, via a fee payer expression resolvable from block fields. -- Optionally reference the applicable fee standard (e.g., ICRC-107) to specify **where the fee goes** (burn, treasury, etc.). +- **Assign a unique `btype` string** for that block type. + This identifier determines how the block is interpreted. + +- **Do not define or constrain `tx.op`**, because: + - `tx.op` belongs to the standard that defines the *method* which creates the block, + not to the block-type standard. + - A single block type may be produced by multiple methods, potentially from different + standards. + - Some blocks (e.g., system-generated events or migration markers) do not include + `tx.op` at all. + + +- **Specify the minimal structure** required to interpret the block and recover its + semantic meaning. + This structure MUST contain all fields needed to reconstruct the event’s effect. + +- **Define precise semantics** for the block using the + *Semantics of Blocks: Evaluation Model*: + - pre-fee state transition, + - fee payer (if applicable), + - validity conditions, + - any invariants or constraints. + + +- **Describe how fees are handled**, if the block type involves fees: + - The standard **SHOULD** specify the **effective fee** (the fee that is actually charged), where this is well-defined. + - The standard **SHOULD** specify the **fee payer**, as an expression resolvable from fields in the block, where this is possible. + - If applicable, the standard **MAY** reference ICRC-107 to specify **where the fee goes** (e.g., burned, sent to a collector, etc.). + + + +- **Allow additional non-semantic fields** in blocks of this type (e.g., metadata, + hashes, memo fields), provided they do not change the semantic interpretation. + The block-type specification MUST clearly identify which fields—whether in tx or at +the top level—are semantic (i.e., affect the block’s meaning). Generic interpreters +MAY ignore any other fields. + +- **Not define or constrain `tx.op`**, because: + - `tx.op` is owned by the standard **defining the method**, not the block type. + - A given block type may be produced by methods from different standards. + - Some blocks (e.g., system-generated blocks) may not include `tx.op` at all. + +This separation ensures: +- Block types define *what* a block means. +- Methods define *how* blocks are formed. +- The same block type can be produced by multiple standards without semantic ambiguity. ### Standards That Define Methods A standard that defines a method which produces blocks MUST: @@ -545,6 +603,10 @@ A standard that defines a method which produces blocks MUST: - For methods that represent user-initiated calls, include an `op` field in `tx` (namespaced as described above) to identify the operation and avoid collisions. +- A namespaced `tx.op` MUST uniquely identify the standardized method that created the block. + The `btype` identifies the semantic family of the block; `tx.op` identifies the specific + method invocation. + This division of responsibility ensures that: - Block types define **what blocks mean** (semantics). @@ -553,13 +615,38 @@ This division of responsibility ensures that: #### Namespacing for Operations -To avoid collisions across standards, `tx.op` MUST be namespaced: -- `op = icrc_number op_name` -- `icrc_number`: a non-zero digit followed by zero or more digits -- `op_name`: starts with a lowercase letter, then lowercase letters, digits, `_` or `-` -**Examples:** `1transfer`, `2transfer_from`, `123freeze_account`. -Legacy ICRC-1/2 blocks are not retrofitted with namespaced `op` values; they retain their historical operation names (e.g., `"xfer"`, `"mint"`, `"burn"`). +The namespacing rules apply to **standards that define user-callable methods**, not to the +standards that define block types. + +If a standard defines a method that produces blocks, and those blocks include a `tx.op`, +then: + +- `tx.op` MUST be namespaced using the ICRC number of the **method’s standard**, not the + block-type standard. +- The value of `op` MUST uniquely identify the method that created the block. +- The pair `(btype, tx.op)` MUST uniquely determine the method invocation that produced the block. + +Formally: + +- `op = ` +- `method_standard_number`: a non-zero digit followed by zero or more digits +- `operation_name`: starts with a lowercase letter, then lowercase letters, digits, `_`, or `-` + +**Examples** +If ICRC-107 defines a user-callable method `set_fee_collector`, and that method produces +blocks of type `107feecol`, then: + +- `btype = "107feecol"` is defined by the **block-type standard** (ICRC-107) +- `tx.op = "107set_fee_collector"` is defined by the **method standard** (also ICRC-107) + +If a method in ICRC-122 produces blocks of type `122freeze`, then: + +- `btype = "122freeze"` +- `tx.op = "122freeze_account"` + +Legacy ICRC-1 and ICRC-2 blocks continue to use their historical operation names +(`"xfer"`, `"mint"`, `"burn"`, `"approve"`) and are exempt from namespacing. ### Note on Fees From 7fe04c62f0d08198217a2503833040a57f01d108 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 13:54:16 +0100 Subject: [PATCH 34/38] refined Semantics of Blocks section --- standards/ICRC-3/README.md | 60 +++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 6b35844..7b622e3 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -274,7 +274,9 @@ If a block represents the result of a **standardized user-initiated method call* - The block **SHOULD** include a `tx` field. - For user-initiated blocks, a namespaced `tx.op` **SHOULD** be included (see above). -- The structure of `tx` **MUST** follow the canonical mapping defined by the relevant standard. +- The `tx` field MUST follow the canonical mapping defined by the method standard. + Semantic fields required by the block type may appear **outside** `tx` when defined + as top-level fields by the block-type specification (e.g., `fee`, `caller`). - All parameters explicitly provided by the caller **MUST** appear in `tx` exactly as provided. - Optional parameters that were not passed in the call **MUST NOT** appear in `tx`. @@ -305,40 +307,53 @@ without over-constraining block structure or requiring the presence of `tx` or ` To ensure consistency across standards and implementations, the semantics of any block must be interpretable through the following evaluation model. Each standard that defines a block type specifies how to “plug into” this model (by defining -its minimal `tx` schema, pre-fee transition, fee payer, etc.). For block types +its minimal schema, pre-fee transition, fee payer, etc.). For block types that do not use a `tx` field, the standard MUST specify how to interpret the block directly from its top-level fields. - 1. Identify block type • If `btype` is present, use it. • If no `btype`, fall back to legacy ICRC-1/2 inference from `tx.op`. -2. Validate `tx` structure - • Check that all semantic fields that the block type specification - marks as required, and that they have the expected shape. - • If the block type specification allows additional optional or - extension fields, they MAY be present. - • ICRC-3 itself does not distinguish between “semantic” and - “non-semantic” fields; it is the responsibility of the block type - specification to state which fields affect the meaning of the block - and how extra fields are to be treated (e.g., ignored by generic - interpreters). +2. Validate block fields + • Check that all semantic fields required by the block-type specification are + present. + • Semantic fields may appear **inside `tx`** or **at the top level**, depending on the + block-type specification. + • Additional non-semantic fields MAY appear anywhere in the block and MUST be + ignored by generic interpreters. + • ICRC-3 itself does not distinguish between “semantic” and “non-semantic” + fields; it is the responsibility of the block type specification to state + which fields affect the meaning of the block and how extra fields are to be + treated (e.g., ignored by generic interpreters). 3. Derive pre-fee state transition - • Apply the deterministic state change implied by `tx`, ignoring any fees. - • Example: debit/credit balances, mint, burn, update allowance. + • Using the semantic fields identified by the block-type specification + (typically the contents of `tx`, plus any semantic top-level fields such as + `caller`, `fee`, etc.), apply the deterministic state change implied by the + block, **ignoring any fees**. + • For block types that do not use `tx`, the standard MUST describe how to + derive this transition directly from the top-level fields. + • Examples: debit/credit balances, mint, burn, update allowance, configuration + change, etc. 4. Apply fee (if applicable) - • If the block type involves fees, determine the **effective fee** according to the rules defined for that block type. - • Deduct the fee from the account designated as the **fee payer** for this block type. - • Adjust balances accordingly (e.g., for mints: `to` receives `amt - fee`). - • The destination or handling of the fee (burn, treasury, etc.) may be specified by the block type or by a separate fee standard (e.g., ICRC-107). When unspecified, the destination/handling of the fee is ledger-defined; ledgers may burn fees or route them to a treasury. See ICRC-107 for a standardized way to expose fee handling. - + • If the block type involves fees, determine the **effective fee** according + to the rules defined for that block type (or a referenced fee standard). + • Deduct the fee from the account designated as the **fee payer** for this + block type. + • Adjust balances accordingly (e.g., for mints: `to` receives `amt - fee`). + • The destination or handling of the fee (burn, treasury, etc.) may be + specified by the block type or by a separate fee standard (e.g., ICRC-107). + When unspecified, the destination/handling of the fee is + implementation- or ledger-defined; ledgers may, for example, burn fees or + route them to a treasury. 5. Enforce validity conditions - • Validate that all preconditions and invariants defined by the block type’s standard are satisfied. - • This includes checks such as sufficient balances, allowance coverage, or limits on fees, as applicable. + • Validate that all preconditions and invariants defined by the block type’s + standard are satisfied. + • This includes checks such as sufficient balances, allowance coverage, or + limits on fees, as applicable. ## Methods @@ -625,7 +640,6 @@ then: - `tx.op` MUST be namespaced using the ICRC number of the **method’s standard**, not the block-type standard. - The value of `op` MUST uniquely identify the method that created the block. -- The pair `(btype, tx.op)` MUST uniquely determine the method invocation that produced the block. Formally: From 247649006d449c0906d00182f4baf07dcbcec2d4 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 15:03:30 +0100 Subject: [PATCH 35/38] refined Namespacing for Operations --- standards/ICRC-3/README.md | 129 ++++++++++++++++++++++++++----------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 7b622e3..e04fbc8 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -345,9 +345,7 @@ block directly from its top-level fields. • Adjust balances accordingly (e.g., for mints: `to` receives `amt - fee`). • The destination or handling of the fee (burn, treasury, etc.) may be specified by the block type or by a separate fee standard (e.g., ICRC-107). - When unspecified, the destination/handling of the fee is - implementation- or ledger-defined; ledgers may, for example, burn fees or - route them to a treasury. + When unspecified, the destination/handling of the fee is implementation- or ledger-defined; ledgers may, for example, burn fees or route them to a treasury. 5. Enforce validity conditions • Validate that all preconditions and invariants defined by the block type’s @@ -565,18 +563,19 @@ A standard that defines a new block type MUST: - **Assign a unique `btype` string** for that block type. This identifier determines how the block is interpreted. -- **Do not define or constrain `tx.op`**, because: - - `tx.op` belongs to the standard that defines the *method* which creates the block, - not to the block-type standard. - - A single block type may be produced by multiple methods, potentially from different - standards. - - Some blocks (e.g., system-generated events or migration markers) do not include - `tx.op` at all. - +- **Not define or constrain `tx.op`**, because: + - `tx.op` belongs to the standard that defines the *method* which creates the + block, not to the block-type standard. + - A single block type may be produced by multiple methods, potentially from + different standards. + - Some blocks (e.g., system-generated events or migration markers) do not + include `tx.op` at all. - **Specify the minimal structure** required to interpret the block and recover its semantic meaning. - This structure MUST contain all fields needed to reconstruct the event’s effect. + This structure MUST contain all fields needed to reconstruct the event’s + effect. These semantic fields may live inside `tx`, at the top level, or be + split between the two, as explicitly defined by the block-type specification. - **Define precise semantics** for the block using the *Semantics of Blocks: Evaluation Model*: @@ -585,49 +584,103 @@ A standard that defines a new block type MUST: - validity conditions, - any invariants or constraints. - - **Describe how fees are handled**, if the block type involves fees: - - The standard **SHOULD** specify the **effective fee** (the fee that is actually charged), where this is well-defined. - - The standard **SHOULD** specify the **fee payer**, as an expression resolvable from fields in the block, where this is possible. - - If applicable, the standard **MAY** reference ICRC-107 to specify **where the fee goes** (e.g., burned, sent to a collector, etc.). - - + - The standard **SHOULD** specify the **effective fee** (the fee that is + actually charged), where this is well-defined. + - The standard **SHOULD** specify the **fee payer**, as an expression + resolvable from fields in the block, where this is possible. + - If applicable, the standard **MAY** reference ICRC-107 to specify **where + the fee goes** (e.g., burned, sent to a collector, etc.). + - If some fee-related aspects are intentionally left implementation- or + ledger-defined, the standard SHOULD say so explicitly. - **Allow additional non-semantic fields** in blocks of this type (e.g., metadata, hashes, memo fields), provided they do not change the semantic interpretation. - The block-type specification MUST clearly identify which fields—whether in tx or at -the top level—are semantic (i.e., affect the block’s meaning). Generic interpreters -MAY ignore any other fields. - -- **Not define or constrain `tx.op`**, because: - - `tx.op` is owned by the standard **defining the method**, not the block type. - - A given block type may be produced by methods from different standards. - - Some blocks (e.g., system-generated blocks) may not include `tx.op` at all. + The block-type specification MUST clearly identify which fields—whether in `tx` + or at the top level—are semantic (i.e., affect the block’s meaning). Generic + interpreters MAY ignore any other fields. This separation ensures: - Block types define *what* a block means. - Methods define *how* blocks are formed. -- The same block type can be produced by multiple standards without semantic ambiguity. +- The same block type can be produced by multiple standards without semantic + ambiguity. ### Standards That Define Methods + A standard that defines a method which produces blocks MUST: + - Specify which `btype` (if any) the method produces. -- Define the canonical mapping from method inputs to the `tx` field of the resulting block. -- Ensure all required fields from the block type’s minimal schema are populated. -- Include only caller-provided optional fields; omit optionals that were not supplied. +- Define the canonical mapping from method inputs to the `tx` field of the + resulting block (and any method-specific top-level fields, if the block-type + specification requires them). +- Ensure all required semantic fields from the block type’s minimal schema are + populated, whether they live inside `tx` or at the top level. +- Include only caller-provided optional fields in `tx`; omit optionals that were + not supplied. - For methods that represent user-initiated calls, include an `op` field in - `tx` (namespaced as described above) to identify the operation and avoid - collisions. -- A namespaced `tx.op` MUST uniquely identify the standardized method that created the block. - The `btype` identifies the semantic family of the block; `tx.op` identifies the specific - method invocation. - + `tx` (namespaced as described below) to identify the operation and avoid + collisions. +- Ensure that a namespaced `tx.op` **uniquely identifies the standardized + method** that created the block within the ICRC namespace. This division of responsibility ensures that: -- Block types define **what blocks mean** (semantics). -- Methods define **how blocks are created** (intent capture). + +- Block types define **what blocks mean** (semantics). +- Methods define **how blocks are created** (intent capture). - Tooling and clients can rely on predictable, non-colliding `tx` values. +#### Namespacing for Operations + +The namespacing rules apply to **standards that define user-callable methods**, not to the +standards that define block types. + +If a standard defines a method that produces blocks, and those blocks include a `tx.op`, +then: + +- `tx.op` MUST be namespaced using the ICRC number of the **method’s standard**, not the + block-type standard. +- The value of `op` MUST uniquely identify the standardized method that created the + block within the global ICRC namespace. + +Formally: + +- `op = ` +- `method_standard_number`: a non-zero digit followed by zero or more digits +- `operation_name`: starts with a lowercase letter, then lowercase letters, digits, + `_`, or `-` + +**Examples** +If ICRC-107 defines a user-callable method `set_fee_collector`, and that method produces +blocks of type `107feecol`, then: + +- `btype = "107feecol"` is defined by the **block-type standard** (ICRC-107) +- `tx.op = "107set_fee_collector"` is defined by the **method standard** (also ICRC-107) + +If a method in ICRC-122 produces blocks of type `122freeze`, then: + +- `btype = "122freeze"` +- `tx.op = "122freeze_account"` + +Legacy ICRC-1 and ICRC-2 blocks continue to use their historical operation names +(`"xfer"`, `"mint"`, `"burn"`, `"approve"`) and are exempt from namespacing. + +### Note on Fees + +ICRC-3 standardizes how fees are recorded in blocks, but it does not prescribe how fees are +calculated or collected. + +- Every standard that introduces a block type involving fees **SHOULD** specify who the + fee payer is and, where possible, how the effective fee is determined, so that + responsibility is unambiguous. +- Where this cannot be expressed generically (for example, because the policy is + intentionally ledger-defined), the standard SHOULD state that the fee payer and/or + fee amount are implementation- or ledger-specific. +- The rules for interpreting the amount and destination of fees MAY be delegated to + ICRC-107 (Fee Handling in Blocks). Ledgers that do not yet implement ICRC-107 MAY + still produce valid ICRC-3 blocks, but their fee behavior will remain + implementation-specific until aligned with ICRC-107. + #### Namespacing for Operations From bb536702e9cac0ca3e1a381623548f53677dc6a6 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 15:12:14 +0100 Subject: [PATCH 36/38] removed duplicate sections --- standards/ICRC-3/README.md | 42 +------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index e04fbc8..8ca0404 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -522,7 +522,7 @@ service : { ### `icrc3_supported_block_types` Returns the set of block types (`btype` identifiers) that the producer canister may emit in its block log. -Producers that emit **legacy ICRC-1/ICRC-2 semantics** using the legacy untyped format MUST still include the corresponding legacy `btype` strings (e.g., `"1mint"`, `"1xfer"`, `"1burn"`, `"2approve"`, …). +Producers that emit legacy ICRC-1/ICRC-2 semantics using the legacy untyped format MUST still include the corresponding legacy btype strings in the return value of `icrc3_supported_block_types`, even though the blocks themselves do not carry a `btype` field. ``` @@ -682,46 +682,6 @@ calculated or collected. implementation-specific until aligned with ICRC-107. -#### Namespacing for Operations - -The namespacing rules apply to **standards that define user-callable methods**, not to the -standards that define block types. - -If a standard defines a method that produces blocks, and those blocks include a `tx.op`, -then: - -- `tx.op` MUST be namespaced using the ICRC number of the **method’s standard**, not the - block-type standard. -- The value of `op` MUST uniquely identify the method that created the block. - -Formally: - -- `op = ` -- `method_standard_number`: a non-zero digit followed by zero or more digits -- `operation_name`: starts with a lowercase letter, then lowercase letters, digits, `_`, or `-` - -**Examples** -If ICRC-107 defines a user-callable method `set_fee_collector`, and that method produces -blocks of type `107feecol`, then: - -- `btype = "107feecol"` is defined by the **block-type standard** (ICRC-107) -- `tx.op = "107set_fee_collector"` is defined by the **method standard** (also ICRC-107) - -If a method in ICRC-122 produces blocks of type `122freeze`, then: - -- `btype = "122freeze"` -- `tx.op = "122freeze_account"` - -Legacy ICRC-1 and ICRC-2 blocks continue to use their historical operation names -(`"xfer"`, `"mint"`, `"burn"`, `"approve"`) and are exempt from namespacing. - - -### Note on Fees -ICRC-3 standardizes how fees are recorded in blocks, but it does not prescribe how fees are calculated or collected. -Every standard that introduces a block type involving fees MUST specify who the fee payer is so that responsibility is unambiguous. -The rules for interpreting the amount and destination of fees are defined in ICRC-107 (Fee Handling in Blocks). Ledgers that do not yet implement ICRC-107 MAY still produce valid ICRC-3 blocks, but their fee behavior will be ledger-specific until aligned with ICRC-107. - - ## Supported Standards From cd0374cc96663ba2ca937acce9998fec1c3a3de2 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 15:30:18 +0100 Subject: [PATCH 37/38] minor rewording --- standards/ICRC-3/README.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 8ca0404..2625800 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -395,8 +395,10 @@ service : { - Archives MUST be returned in strictly increasing order of their start index. - If `from` is `null`, the producer MUST return the first archive(s). -- If `from` is set, the producer MUST return archives whose `canister_id` - appears after the given principal in the producer’s archive ordering. +If `from` is set, the producer MUST return only the archives whose +`canister_id` appears *after* the specified principal in the producer’s +internal archive ordering. + - For every archive returned: - `start` and `end` MUST describe a contiguous, inclusive block range; - that archive MUST be able to serve exactly that range via its callbacks (as returned indirectly in `icrc3_get_blocks`). @@ -471,10 +473,12 @@ service : { - Each entry in `archived_blocks`: - MUST have `args` describing one or more contiguous ranges of archived indices, and - - MUST provide a `callback` which, when called with a subset of those ranges, - returns blocks whose `id` values lie within the requested ranges, again - subject to the same kinds of constraints (message size, security limits, - reaching that archive’s tip, etc.). +- MUST provide a `callback` which, when called with any subset of the + ranges listed in `args`, returns the blocks whose `id` values lie within + the requested ranges, except where constraints such as message size, + security limits, or the archive’s own tip boundaries prevent returning + the full set. + - Implementations MAY therefore return only a **partial view** of the blocks in the requested ranges. Clients MUST be prepared to receive fewer blocks than @@ -1205,7 +1209,7 @@ variant { }; ``` -#### Example 6: Typed block (`btype = "107fee"`) +#### Example 6: Typed block (`btype = "107feecol"`) ``` @@ -1217,12 +1221,12 @@ variant { Map = vec { record { "tx"; variant { Map = vec { record { "op"; variant { Text = "107set_fee_collector" }}; record { "fee_collector"; variant { Array = vec { }}}; // [] means "burn from now on" - record { "created_at_time"; variant { Nat = 1_750_951_728_000_000_000 : nat }}; + record { "created_at_time"; variant { nat = 1_750_951_728_000_000_000 : nat }}; record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\00\00\01\01" }}; }}}; // Standard block metadata - record { "ts"; variant { Nat = 1_741_312_737_184_874_392 : nat }}; + record { "ts"; variant { nat = 1_741_312_737_184_874_392 : nat }}; record { "phash"; variant { Blob = blob "\2d\86\7f\34\c7\2d\1e\2d\00\84\10\a4\00\b0\b6\4c\3e\02\96\c9\e8\55\6f\dd\72\68\e8\df\8d\8e\8a\ee" }}; }} ``` From 6194432f7d649c7c441fdd187bd7e6d0d2aba0c7 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Fri, 21 Nov 2025 16:35:31 +0100 Subject: [PATCH 38/38] Candid spec --- standards/ICRC-3/README.md | 147 +++++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 24 deletions(-) diff --git a/standards/ICRC-3/README.md b/standards/ICRC-3/README.md index 2625800..9ae17d6 100644 --- a/standards/ICRC-3/README.md +++ b/standards/ICRC-3/README.md @@ -368,10 +368,10 @@ Clients use `from` to request only archives that appear after a previously seen ``` type GetArchivesArgs = record { - // The last archive seen by the client. - // The producer will return archives coming - // after this one if set, otherwise it - // will return the first archives. +// The producer will return archives that appear after +// this one in its internal archive ordering if set, +// otherwise it will return the first archives. + from : opt principal; }; @@ -395,7 +395,7 @@ service : { - Archives MUST be returned in strictly increasing order of their start index. - If `from` is `null`, the producer MUST return the first archive(s). -If `from` is set, the producer MUST return only the archives whose +- If `from` is set, the producer MUST return only the archives whose `canister_id` appears *after* the specified principal in the producer’s internal archive ordering. @@ -473,11 +473,11 @@ service : { - Each entry in `archived_blocks`: - MUST have `args` describing one or more contiguous ranges of archived indices, and -- MUST provide a `callback` which, when called with any subset of the - ranges listed in `args`, returns the blocks whose `id` values lie within - the requested ranges, except where constraints such as message size, - security limits, or the archive’s own tip boundaries prevent returning - the full set. + - MUST provide a `callback` which, when called with any subset of the + ranges listed in `args`, returns the blocks whose `id` values lie within + the requested ranges, except where constraints such as message size, + security limits, or the archive’s own tip boundaries prevent returning + the full set. - Implementations MAY therefore return only a **partial view** of the blocks in @@ -697,7 +697,104 @@ An ICRC-3 compatible producer canister MUST expose an endpoint listing all the s +## Candid Specification + +``` +// ICRC-3: Block Log Candid Interface + +// Generic representation-independent value type used for blocks. +type Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Int : int; + Array : vec Value; + Map : vec record { text; Value }; +}; + +// Archives + +type GetArchivesArgs = record { + // The last archive seen by the client. + // The producer will return archives that appear after + // this one in its internal archive ordering if set, + // otherwise it will return the first archives. + from : opt principal; +}; + +type GetArchivesResult = vec record { + // The id of the archive canister + canister_id : principal; + + // The first block in the archive (inclusive) + start : nat; + + // The last block in the archive (inclusive) + end : nat; +}; + +// Blocks + +// Each element describes a half-open range [start, start + length) +type GetBlocksArgs = vec record { + start : nat; + length : nat; +}; + +type GetBlocksResult = record { + // Total number of blocks in the block log + log_length : nat; + + // Blocks found locally on the producer canister + blocks : vec record { + id : nat; // block index + block : Value; // encoded block + }; + + // Callbacks to fetch archived blocks + archived_blocks : vec record { + // Archived ranges available via this callback + args : GetBlocksArgs; + + // Callback to fetch a subset of the above ranges + callback : func (GetBlocksArgs) -> (GetBlocksResult) query; + }; +}; + +// Tip certificate + +// See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification +type DataCertificate = record { + // Signature of the root of the hash_tree + certificate : blob; + + // CBOR-encoded hash_tree + hash_tree : blob; +}; + +// Supported block types + +type SupportedBlockType = record { + block_type : text; // e.g. "1xfer", "107feecol" + url : text; // canonical URL of the defining spec +}; + +// ICRC-3 service + +service : { + // Returns archive metadata + icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; + + // Returns blocks and archive callbacks for one or more ranges + icrc3_get_blocks : (GetBlocksArgs) -> (GetBlocksResult) query; + + // Returns a certificate for the tip of the block log + icrc3_get_tip_certificate : () -> (opt DataCertificate) query; + // Returns the set of block types this producer may emit + icrc3_supported_block_types : () -> (vec SupportedBlockType) query; +} +``` @@ -951,19 +1048,19 @@ This example shows an `icrc1_transfer` call where the caller only specifies the ``` variant { Map = vec { - record { "fee"; variant { nat = 10_000 : nat } }; + record { "fee"; variant { Nat = 10_000 : nat } }; record { "phash"; variant { Blob = blob "\b8\0d\29\e5\91\60\4c\d4\60\3a\2a\7c\c5\33\14\21\27\b8\23\e9\a5\24\b7\14\43\24\4b\2d\d5\b0\86\13" }; }; - record { "ts"; variant { nat = 1_753_344_727_778_561_060 : nat } }; + record { "ts"; variant { Nat = 1_753_344_727_778_561_060 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { nat = 85_224_322_205 : nat } }; + record { "amt"; variant { Nat = 85_224_322_205 : nat } }; record { "from"; variant { Array = vec { variant { Blob = blob "\00\00\00\00\02\30\02\17\01\01" } } } }; record { "op"; variant { Text = "xfer" } }; record { @@ -995,12 +1092,12 @@ variant { Blob = blob "\c2\b1\32\6a\5e\09\0e\10\ad\be\f3\4c\ba\fd\bc\90\18\3f\38\a7\3e\73\61\cc\0a\fa\99\89\3d\6b\9e\47" }; }; - record { "ts"; variant { nat = 1_753_344_737_123_456_789 : nat } }; + record { "ts"; variant { Nat = 1_753_344_737_123_456_789 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { nat = 500_000_000 : nat } }; + record { "amt"; variant { Nat = 500_000_000 : nat } }; record { "to"; variant { @@ -1031,12 +1128,12 @@ variant { Blob = blob "\7f\89\42\a5\be\4d\af\50\3b\6e\2a\8e\9c\c7\dd\f1\c9\e8\24\f0\98\bb\d7\af\ae\d2\90\10\67\df\1e\c1\0a" }; }; - record { "ts"; variant { nat = 1_753_344_740_000_000_000 : nat } }; + record { "ts"; variant { Nat = 1_753_344_740_000_000_000 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { nat = 42_000_000 : nat } }; + record { "amt"; variant { Nat = 42_000_000 : nat } }; record { "from"; variant { @@ -1119,19 +1216,19 @@ This example shows an `icrc2_transfer_from` call where the recipient is a regula ``` variant { Map = vec { - record { "fee"; variant { nat = 10_000 : nat } }; + record { "fee"; variant { Nat = 10_000 : nat } }; record { "phash"; variant { Blob = blob "\a0\5f\d2\f3\4c\26\73\58\00\7f\ea\02\18\43\47\70\85\50\2e\d2\1f\23\e0\dc\e6\af\3c\cf\9e\6f\4a\d8" }; }; - record { "ts"; variant { nat = 1_753_344_728_820_625_931 : nat } }; + record { "ts"; variant { Nat = 1_753_344_728_820_625_931 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { nat = 50_419_165_435 : nat } }; + record { "amt"; variant { Nat = 50_419_165_435 : nat } }; record { "from"; variant { @@ -1178,12 +1275,12 @@ variant { Blob = blob "\9a\cd\20\3f\b0\11\fb\7f\e2\2a\1d\f2\c1\dd\22\6a\2f\1e\f6\88\d3\b0\9f\be\8d\2e\c5\70\f2\b4\a1\77" }; }; - record { "ts"; variant { nat = 1_753_344_750_000_000_000 : nat } }; + record { "ts"; variant { Nat = 1_753_344_750_000_000_000 : nat } }; record { "tx"; variant { Map = vec { - record { "amt"; variant { nat = 200_000 : nat } }; + record { "amt"; variant { Nat = 200_000 : nat } }; record { "from"; variant { @@ -1221,13 +1318,15 @@ variant { Map = vec { record { "tx"; variant { Map = vec { record { "op"; variant { Text = "107set_fee_collector" }}; record { "fee_collector"; variant { Array = vec { }}}; // [] means "burn from now on" - record { "created_at_time"; variant { nat = 1_750_951_728_000_000_000 : nat }}; + record { "created_at_time"; variant { Nat = 1_750_951_728_000_000_000 : nat }}; record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\00\00\01\01" }}; }}}; // Standard block metadata - record { "ts"; variant { nat = 1_741_312_737_184_874_392 : nat }}; + record { "ts"; variant { Nat = 1_741_312_737_184_874_392 : nat }}; record { "phash"; variant { Blob = blob "\2d\86\7f\34\c7\2d\1e\2d\00\84\10\a4\00\b0\b6\4c\3e\02\96\c9\e8\55\6f\dd\72\68\e8\df\8d\8e\8a\ee" }}; }} ``` + +