diff --git a/ICRCs/BUILD.bazel b/ICRCs/BUILD.bazel new file mode 100644 index 00000000..a094347a --- /dev/null +++ b/ICRCs/BUILD.bazel @@ -0,0 +1,25 @@ +load("//bazel:didc_test.bzl", "didc_subtype_test") + +exports_files([ + "ICRC-2.did", +]) + +genrule( + name = "candid", + srcs = [":README.md"], + outs = ["ICRC-2-generated.did"], + cmd_bash = "$(location @lmt) $(SRCS); mv ICRC-2.did $@", + exec_tools = ["@lmt"], +) + +didc_subtype_test( + name = "check_generated_subtype", + did = ":ICRC-2-generated.did", + previous = "ICRC-2.did", +) + +didc_subtype_test( + name = "check_source_subtype", + did = "ICRC-2.did", + previous = ":ICRC-2-generated.did", +) diff --git a/ICRCs/HASHINGVALUES.md b/ICRCs/HASHINGVALUES.md new file mode 100644 index 00000000..52489cf2 --- /dev/null +++ b/ICRCs/HASHINGVALUES.md @@ -0,0 +1,102 @@ +## Representation independent hashing + +The following pseudocode specifies how to calculate the (representation independent) hash of an element of the Value type. Some test vectors to check compliance of an implementation with this specification follow. + +``` +type Value = variant { + Blob : blob, + Text : text, + Nat : nat, + Int : int, + Array : vec Value, + Map : vec (text, Value) +}; + +Function hash_value(value) + Initialize hasher as a new instance of SHA256 + + Match value with + Nat: + Return SHA256_hash(LEB128_encode(value)) + Int: + Return SHA256_hash(SLEB128_encode(value)) + Text: + Return SHA256_hash(UTF8_encode(value)) + Blob: + Return SHA256_hash(value) + Array: + For each element in value + Update hasher with hash_value(element) + Return hasher.finalize() + Map: + Initialize hashes as empty list + For each (key, val) in value + Add (SHA256_hash(UTF8_encode(key)), hash_value(val)) to hashes + Sort hashes in lexicographical order + For each (key_hash, val_hash) in hashes + Update hasher with key_hash + Update hasher with val_hash + Return hasher.finalize() + Else: + Return error "unsupported value type" +End Function + +Function LEB128_encode(nat_input) + Convert nat_input to LEB128 byte encoding +End Function + +Function SLEB128_encode(integer_input) + Convert integer_input to SLEB128 byte encoding +End Function + +Function UTF8_encode(text) + Convert text to UTF-8 byte array and return it +End Function + +Function SHA256_hash(data) + Initialize a new SHA256 hasher + Update hasher with data + Return hasher.finalize() +End Function + +``` + +## Test vectors + + +```ignorelang +input: Nat(42) +expected output: 684888c0ebb17f374298b65ee2807526c066094c701bcc7ebbe1c1095f494fc1 +``` + +```ignorelang +input: Int(-42) +expected output: de5a6f78116eca62d7fc5ce159d23ae6b889b365a1739ad2cf36f925a140d0cc +``` + + +```ignorelang +input: Text("Hello, World!"), +expected output: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f +``` + +```ignorelang +input: Blob(b'\x01\x02\x03\x04') +expected output: 9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a +``` + +```ignorelang +input: Array([Nat(3), Text("foo"), Blob(b'\x05\x06')]) +expected output: 514a04011caa503990d446b7dec5d79e19c221ae607fb08b2848c67734d468d6 +``` + +```ignorelang +input: Map([("from", Blob(b'\x00\xab\xcd\xef\x00\x12\x34\x00\x56\x78\x9a\x00\xbc\xde\xf0\x00\x01\x23\x45\x67\x89\x00\xab\xcd\xef\x01')), + ("to", Blob(b'\x00\xab\x0d\xef\x00\x12\x34\x00\x56\x78\x9a\x00\xbc\xde\xf0\x00\x01\x23\x45\x67\x89\x00\xab\xcd\xef\x01')), + ("amount", Nat(42)), + ("created_at", Nat(1699218263)), + ("memo", Nat(0)) + ]) + +expected output: c56ece650e1de4269c5bdeff7875949e3e2033f85b2d193c2ff4f7f78bdcfc75 +``` \ No newline at end of file diff --git a/ICRCs/ICRC-1.did b/ICRCs/ICRC-1.did new file mode 100644 index 00000000..f9307ca8 --- /dev/null +++ b/ICRCs/ICRC-1.did @@ -0,0 +1,52 @@ +// Number of nanoseconds since the UNIX epoch in UTC timezone. +type Timestamp = nat64; + +// Number of nanoseconds between two [Timestamp]s. +type Duration = nat64; + +type Subaccount = blob; + +type Account = record { + owner : principal; + subaccount : opt Subaccount; +}; + +type TransferArgs = record { + from_subaccount : opt Subaccount; + to : Account; + amount : nat; + fee : opt nat; + memo : opt blob; + created_at_time : opt Timestamp; +}; + +type TransferError = variant { + BadFee : record { expected_fee : nat }; + BadBurn : record { min_burn_amount : nat }; + InsufficientFunds : record { balance : nat }; + TooOld; + CreatedInFuture: record { ledger_time : Timestamp }; + Duplicate : record { duplicate_of : nat }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type Value = variant { + Nat : nat; + Int : int; + Text : text; + Blob : blob; +}; + +service : { + icrc1_metadata : () -> (vec record { text; Value; }) query; + icrc1_name : () -> (text) query; + icrc1_symbol : () -> (text) query; + icrc1_decimals : () -> (nat8) query; + icrc1_fee : () -> (nat) query; + icrc1_total_supply : () -> (nat) query; + icrc1_minting_account : () -> (opt Account) query; + icrc1_balance_of : (Account) -> (nat) query; + icrc1_transfer : (TransferArgs) -> (variant { Ok : nat; Err : TransferError }); + icrc1_supported_standards : () -> (vec record { name : text; url : text }) query; +} diff --git a/ICRCs/ICRC-124/ICRC-124.md b/ICRCs/ICRC-124/ICRC-124.md new file mode 100644 index 00000000..90b00654 --- /dev/null +++ b/ICRCs/ICRC-124/ICRC-124.md @@ -0,0 +1,198 @@ +# ICRC-124: Pause, Unpause & Deactivate Blocks + +## Status + +Draft + +## Introduction + +This standard defines new block types for recording administrative actions that control the operational state of an ICRC-compliant ledger: pausing (`124pause`), unpausing (`124unpause`), and deactivating (`124deactivate`). These actions allow ledger operations to be temporarily halted (e.g., for maintenance), resumed, or permanently stopped (making the ledger immutable). This standard provides a consistent, auditable way to represent these ledger-wide state transitions within the ledger's block history, ensuring transparency and enabling robust governance mechanisms. + +## Motivation + +Ledger lifecycle management may require administrative actions like pausing for upgrades, unpausing after checks, or deactivating at the end of a token's useful life. These significant events must be recorded transparently on-chain. This standard provides explicit block types for these actions, defining a minimal block structure sufficient for semantics, while allowing optional provenance for auditability. + +## Common Elements + +This standard follows the conventions set by ICRC-3, inheriting key structural components: + +- **Principals** are represented using the ICRC-3 `Value` type as `variant { Blob = }`. +- **Timestamps:** `ts` (and any optional `created_at_time`) are **nanoseconds since the Unix epoch**, encoded as `Nat` but **MUST fit into `nat64`**. +- **Parent hash:** `phash : Blob` **MUST** be present if the block has a parent (omit for the genesis block). + +## Block Types & Schema + +Each block introduced by this standard MUST include a `tx` field containing a map. This map encodes the **minimal information** about the ledger state change. Additional provenance MAY be included but is not required for semantics. + +Each block consists of the following top-level fields: + +| Field | Type (ICRC-3 `Value`) | Required | Description | +|-------|------------------------|----------|-------------| +| `btype` | `Text` | Yes | MUST be one of: `"124pause"`, `"124unpause"`, or `"124deactivate"`. | +| `ts` | `Nat` | Yes | Timestamp (ns since Unix epoch) when the block was added to the ledger. MUST fit in `nat64`. | +| `phash` | `Blob` | Yes/No | Hash of the parent block; omitted only for the genesis block. | +| `tx` | `Map(Text, Value)` | Yes | Minimal operation details (see below). | + +### `tx` Field Schema (minimal) + +For all `124pause`, `124unpause`, and `124deactivate` blocks: + +- No required fields are needed for semantics. +- The presence of the block type alone (`btype`) determines the state transition. + +### Optional Provenance (non-semantic) + +Producers MAY include fields such as: + +- `caller : Blob` — principal that invoked the operation. +- `reason : Text` — human-readable context. +- `created_at_time : Nat` — caller-supplied timestamp (ns; MUST fit nat64). +- `policy_ref : Text` — identifier for proposal/vote/policy. +- `op : Text` — namespaced operation identifier, e.g. `148pause_ledger`. + +These fields MUST NOT affect semantics or verification. Verifiers MUST ignore them. + +> **Informative note (recoverability):** Implementations **SHOULD** provide mechanisms (e.g., archives or lookups) to retrieve extended invocation context not present in `tx` when useful for audits. The authorization model that permits these actions is implementation-defined. + +--- + +## Semantics + +### Pause Ledger (`124pause`) +- When a `124pause` block is recorded, the ledger MUST enter a "paused" state. +- While paused, the ledger MUST reject all state-changing operations except those required for governance or recovery (e.g., `124unpause`, optionally `124deactivate`, and operations like freeze/unfreeze if permitted by governance policy). +- Query calls SHOULD remain operational. + +### Unpause Ledger (`124unpause`) +- When a `124unpause` block is recorded, the ledger MUST exit the "paused" state and resume normal operation, unless it is already in the terminal state due to deactivation. +- An `124unpause` block has no effect if the ledger is already unpaused or deactivated. + +### Deactivate Ledger (`124deactivate`) +- When a `124deactivate` block is recorded, the ledger MUST transition to a permanent "terminal" state. +- In this state: + - All ingress calls that modify state MUST be rejected (transfers, approvals, mints, burns, freezes, pauses, unpauses, etc.). + - Query calls retrieving historical data MUST remain available. +- The deactivated state is irreversible. + +--- + +## Guidance for Standards That Define Methods + +A standard that defines ledger methods which produce ICRC-124 blocks (e.g., “pause ledger” or “deactivate ledger”) SHOULD: + +1. **Include `tx.op`** in the resulting block’s `tx` map. + - Use a namespaced value per ICRC-3: `` (e.g., `148pause_ledger`). + - This makes the call uniquely identifiable and prevents collisions across standards. + +2. **Define a canonical mapping** from the method’s call parameters to the block’s minimal `tx` fields. + - Since 124 blocks have no required fields, only provenance may be mapped. + +3. **Document deduplication inputs** (if any). If the method uses a caller-supplied timestamp, put it in `tx.created_at_time`. + +--- + +## Compliance Reporting + +Ledgers implementing this standard MUST return the following entries (along with entries for other supported block types) from `icrc3_supported_block_types`: + +```candid +vec { + record { block_type = "124pause"; url = "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-124.md" }; + record { block_type = "124unpause"; url = "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-124.md" }; + record { block_type = "124deactivate"; url = "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-124.md" }; +} +``` + +## Example Blocks + +### 124pause Example + +```candid +variant { Map = vec { +    // Block type identifier +    record { "btype"; variant { Text = "124pause" }}; + +    // Timestamp when the block was recorded (nanoseconds since epoch) +    record { "ts"; variant { Nat = 1_747_774_560_000_000_000 : nat }}; // Example: 2025-05-19T12:56:00Z + +    // Hash of the previous block in the ledger chain +    record { "phash"; variant { +        Blob = blob "\de\ad\be\ef\00\11\22\33\44\55\66\77\88\99\aa\bb\cc\dd\ee\ff\10\20\30\40\50\60\70\80\90\a0\b0\c0" +    }}; + +    // Pause transaction details +    record { "tx"; variant { Map = vec { + // The principal that invoked the pause_ledger operation +        record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\f0\0d\01\03" }}; // Example caller principal (e.g., a governance canister) +        // Optional reason +        record { "reason"; variant { Text = "DAO vote #78: pause for scheduled maintenance." }}; +    }}}; +}}; + +``` + +### 124unpause Example + +```candid +variant { Map = vec { +    record { "btype"; variant { Text = "124unpause" }}; +    record { "ts"; variant { Nat = 1_747_778_160_000_000_000 : nat }}; // Example: 2025-05-19T13:56:00Z +    record { "phash"; variant { +        Blob = blob "\be\ba\fe\ca\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b" +    }}; +    // Unpause transaction details +    record { "tx"; variant { Map = vec { + // The principal that invoked the unpause_ledger operation +        record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\f0\0d\01\03" }}; // Example caller principal +        // Optional reason +        record { "reason"; variant { Text = "Ledger resumes after maintenance window (DAO vote #79)." }}; +    }}}; +}}; +``` + +### 124deactivate Example + +```candid +variant { Map = vec { +    record { "btype"; variant { Text = "124deactivate" }}; +    record { "ts"; variant { Nat = 1_747_864_560_000_000_000 : nat }}; // Example: 2025-05-20T12:56:00Z +    record { "phash"; variant { +        Blob = blob "\c0\ff\ee\00\10\20\30\40\50\60\70\80\90\a0\b0\c0\d0\e0\f0\00\11\22\33\44\55\66\77\88\99\aa\bb\cc" +    }}; +    // Deactivate transaction details +    record { "tx"; variant { Map = vec { + // The principal that invoked the deactivate_ledger operation +        record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\f0\0d\01\04" }}; // Example caller (e.g., project multisig or final DAO vote) +        // Optional reason +        record { "reason"; variant { Text = "Token project sunset. Ledger permanently archived as per SNS DAO proposal #314." }}; +    }}}; +}}; + +``` + +### Informative Example: Integration with a Standardized Method +ICRC-124 defines only block types and their semantics. It does not define any ledger methods. +However, future standards may specify methods that map directly to these block types. + +For illustration, suppose a future standard (e.g., ICRC-148) introduces the method: +``` +icrc148_pause_ledger : (opt text) -> result nat +``` + +Invoking this method with an optional reason could produce a `124pause` block: +``` +variant { Map = vec { + record { "btype"; variant { Text = "124pause" }}; + record { "ts"; variant { Nat = 1_747_900_000_000_000_000 : nat }}; + record { "phash"; variant { + Blob = blob "\aa\bb\cc\dd\ee\ff\00\11\22\33\44\55\66\77\88\99" + }}; + record { "tx"; variant { Map = vec { + record { "op"; variant { Text = "148pause_ledger" }}; + record { "caller"; variant { Blob = blob "\00\00\00\00\00\00\f0\0d\01\03" }}; + record { "reason"; variant { Text = "DAO vote #101: emergency pause" }}; + }}}; +}}; +``` + +This example is non-normative and illustrates how a standardized method can map into the ICRC-124 block structure while using a namespaced `tx.op` for unambiguous identification. The authoritative semantics remain defined by the ICRC-124 block types. \ No newline at end of file diff --git a/ICRCs/ICRC-2.did b/ICRCs/ICRC-2.did new file mode 100644 index 00000000..153bf49d --- /dev/null +++ b/ICRCs/ICRC-2.did @@ -0,0 +1,62 @@ +type Account = record { + owner : principal; + subaccount : opt blob; +}; + +type ApproveArgs = record { + from_subaccount : opt blob; + spender : Account; + amount : nat; + expected_allowance : opt nat; + expires_at : opt nat64; + fee : opt nat; + memo : opt blob; + created_at_time : opt nat64; +}; + +type ApproveError = variant { + BadFee : record { expected_fee : nat }; + InsufficientFunds : record { balance : nat }; + AllowanceChanged : record { current_allowance : nat }; + Expired : record { ledger_time : nat64 }; + TooOld; + CreatedInFuture: record { ledger_time : nat64 }; + Duplicate : record { duplicate_of : nat }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type TransferFromArgs = record { + spender_subaccount : opt blob; + from : Account; + to : Account; + amount : nat; + fee : opt nat; + memo : opt blob; + created_at_time : opt nat64; +}; + +type TransferFromError = variant { + BadFee : record { expected_fee : nat }; + BadBurn : record { min_burn_amount : nat }; + InsufficientFunds : record { balance : nat }; + InsufficientAllowance : record { allowance : nat }; + TooOld; + CreatedInFuture: record { ledger_time : nat64 }; + Duplicate : record { duplicate_of : nat }; + TemporarilyUnavailable; + GenericError : record { error_code : nat; message : text }; +}; + +type AllowanceArgs = record { + account : Account; + spender : Account; +}; + +service : { + icrc1_supported_standards : () -> (vec record { name : text; url : text }) query; + + icrc2_approve : (ApproveArgs) -> (variant { Ok : nat; Err : ApproveError }); + icrc2_transfer_from : (TransferFromArgs) -> (variant { Ok : nat; Err : TransferFromError }); + icrc2_allowance : (AllowanceArgs) -> (record { allowance : nat; expires_at : opt nat64 }) query; +} diff --git a/ICRCs/ICRC-3.did b/ICRCs/ICRC-3.did new file mode 100644 index 00000000..70c5fc39 --- /dev/null +++ b/ICRCs/ICRC-3.did @@ -0,0 +1,57 @@ +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 Ledger 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 : vec record { id : nat; block: Value }; + + archived_blocks : vec record { + args : GetBlocksArgs; + callback : func (GetBlocksArgs) -> (GetBlocksResult) query; + }; +}; + +type DataCertificate = record { + // See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification + certificate : blob; + + // CBOR encoded hash_tree + hash_tree : blob; +}; + +service : { + icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; + icrc3_get_tip_certificate : () -> (opt DataCertificate) query; + icrc3_get_blocks : (GetBlocksArgs) -> (GetBlocksResult) query; + icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query; +}; diff --git a/ICRCs/README.md b/ICRCs/README.md new file mode 100644 index 00000000..652c5da7 --- /dev/null +++ b/ICRCs/README.md @@ -0,0 +1,814 @@ +# `ICRC-3`: Block Log + +| Status | +|:------:| +| [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` 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 + +## Archive Nodes + +The Ledger must expose an endpoint `icrc3_get_archives` listing all the canisters containing its blocks. + +## 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`. + +``` + ┌─────────────────────────┐ ┌─────────────────────────┐ + | Block i | | Block i+1 | + ├─────────────────────────┤ ├─────────────────────────┤ +◄──| phash = hash(Block i-1) |◄─────────| phash = hash(Block i) | + | ... | | ... | + └─────────────────────────┘ └─────────────────────────┘ + +``` + +## 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 +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. + +For this reason, `ICRC-3` introduces the `Value` type which never changes: + +``` +type Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Int : int; + Array : vec Value; + Map : vec record { text; Value }; +}; +``` + +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. + +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 +- the hash of a `Text` is the hash of the bytes representing the text +- 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. + +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 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 + +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` +2. otherwise the `last_block_hash` in the tip certificate. + +## Generic Block Schema + +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 + + +### 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. 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 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). + +### 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"`). + + +### 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`. + +### 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 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** 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. + + +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. + + + +## Interaction with Other Standards + +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. + +### 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, 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: +- 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. + +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. + + +#### 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"`). + + +### 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 + +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 + + +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. + +Legacy blocks therefore follow a fixed generic structure, with semantics inferred from `tx.op`. + +--- + +#### Generic Legacy Block + +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. + - `"tx"`: `Value::Map` — representing the user’s transaction intent. +- **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"`) + +**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 `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`. + +--- + +#### 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`. +- **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. + +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` and **MUST** be reduced by `tx.amt`. + + +**Fee payer:** `tx.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`. +- **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. + +**Semantics** +Burns remove `tx.amt` tokens from `tx.from`. Any fee is also debited from `tx.from`. + +**Fee payer:** `tx.from`. + +--- + +#### Approve Block (`op = "approve"`) + +**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. + +**Semantics** +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`. + + +**Fee payer:** `tx.from`. + +--- + + + + +### 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: + +- "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 + +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" }; + }; +}; +``` + + +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" }; + }; +}; + +``` + + +### Canonical `tx` Mapping + +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:** + +``` +icrc1_transfer: record { + to: Account; + amount: Nat; + fee: opt Nat; + memo: opt Blob; + from_subaccount: opt blob; + created_at_time: opt Nat; +} +``` + +**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: + +- `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 + + +**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. + + + +### 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. + +``` +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 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 { + "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" } }; + } + }; + }; + } +}; +``` + +--- + +#### 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. + +``` +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" } }; + } + }; + }; + } +}; +``` + +##### `icrc2_transfer_from` + +**Call parameters:** + +``` + 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; +} +``` + +**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` +- `memo = memo` if provided +- `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 + +**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). + +``` +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 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 { + "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` + +``` +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 Ledger 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 Ledger + 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 + 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; +}; +``` diff --git a/ICRCs/TextualEncoding.md b/ICRCs/TextualEncoding.md new file mode 100644 index 00000000..14accb56 --- /dev/null +++ b/ICRCs/TextualEncoding.md @@ -0,0 +1,79 @@ +# Textual encoding of ICRC-1 accounts + +| Status | +|:------:| +| Accepted | + +This document specifies the canonical textual representation of ICRC-1 accounts. + +ICRC-1 accounts have two components: the owner (up to 29 bytes) and the subaccount (32 bytes). +If the subaccount is not present, it's considered to be equal to an array with 32 zero bytes. + +```candid +type Account = { owner : principal; subaccount : opt blob }; +``` + +## Default accounts + +The textual representation of the account coincides with the textual encoding of the account owner's principal if the `subaccount` is not set or equal to an array with 32 zero bytes. + +``` +Account.toText(record { + owner = principal "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae"; + subaccount = null; +}) = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae" +``` + +``` +Account.toText(record { + owner = principal "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + subaccount = opt vec {0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0;0}; +}) = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae" +``` + +## Non-default accounts + +The textual representation of accounts with non-default subaccounts consists of the following parts: + 1. The textual encoding of the owner's principal as described in the [Internet Computer Interface Specification](https://internetcomputer.org/docs/current/references/ic-interface-spec#textual-ids). + 2. A dash ('-') separating the principal from the checksum. + 3. The CRC-32 checksum of concatenated bytes of the principal (up to 29 bytes) and the subaccount (32 bytes), encoded in [Base 32 encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-6), without padding, and using lower-case letters. + 4. A period ('.') separating the checksum from the subaccount. + 5. The hex-encoded bytes of the subaccount with all the leading '0' characters removed. + +``` +-. +``` + +``` +Account.toText({ owner; ?subaccount }) = { + let checksum = bigEndianBytes(crc32(concatBytes(Principal.toBytes(owner), subaccount))); + Principal.toText(owner) # '-' # base32LowerCaseNoPadding(checksum) # '.' # trimLeading('0', hex(subaccount)) +} +``` + +In the following example, `dfxgiyy` is the checksum and `102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20` is the hex representation of the subaccount with stripped leading zeros. + +``` +Account.toText(record { + owner = principal "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", + subaccount = opt vec {1;2;3;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30;31;32}; +}) = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" +``` + +## Examples + +| Text | Result | Comment | +|:----:|:------:|:-------:| +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae` | OK: `{ owner = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", subaccount = null }` | A valid principal is a valid account. | +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-q6bn32y.` | Error | The representation is not canonical: default subaccount should be omitted. | +| `k2t6j2nvnp4zjm3-25dtz6xhaac7boj5gayfoj3xs-i43lp-teztq-6ae` | Error | Invalid principal encoding. | +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.1` | OK: `{ owner = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae", subaccount = opt 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\01" }` | | +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-6cc627i.01` | Error | The representation is not canonical: leading zeros are not allowed in subaccounts. | +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae.1` | Error | Missing check sum. | +| `k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20` | OK: `{ owner = "k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae"; subaccount = opt blob "\01\02\03\04\05\06\07\08\09\0a\0b\0c\0d\0e\0f\10\11\12\13\14\15\16\17\18\19\1a\1b\1c\1d\1e\1f\20" }` | | + +## Libraries + +* [`ic-js`](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc#gear-encodeicrcaccount) (JavaScript). +* [`icrc-ledger-types`](https://docs.rs/icrc-ledger-types/0.1.2/icrc_ledger_types/icrc1/account/struct.Account.html) version `0.1.2` and higher (Rust). +* [`Account.mo`](https://github.com/dfinity/ICRC-1/blob/main/ref/Account.mo) (Motoko)