| Status |
|---|
| Accepted |
ICRC-3 is a standard for accessing the block log of a Ledger on the Internet Computer.
ICRC-3 specifies:
- A way to fetch the archive nodes of a Ledger
- A generic format for sharing the block log without information loss. This includes the fields that a block must have
- A mechanism to verify the block log on the client side to allow downloading the block log via query calls
- A way for new standards to define new transactions types compatible with ICRC-3
The Ledger must expose an endpoint icrc3_get_archives listing all the canisters containing its blocks.
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) |
| ... | | ... |
└─────────────────────────┘ └─────────────────────────┘
The candid format supports sharing information even when the client and the server involved do not have the same schema (see the 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 represent a single block in the block log.
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 used by the IC:
- the hash of a
Blobis the hash of the bytes themselves - the hash of a
Textis the hash of the bytes representing the text - the hash of a
Natis the hash of theleb128encoding of the number - the hash of an
Intis the hash of thesleb128encoding of the number - the hash of an
Arrayis the hash of the concatenation of the hashes of all the elements of the array - the hash of a
Mapis 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.
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. The certificate is comprised of a tree containing the certified data and the signature. The tree MUST contain two labelled values (leafs):
last_block_index: the index of the last block in the chain. The values must be expressed asleb128last_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.
Validation of block i is done by checking the block hash against
- if
i + 1 < len(chain)then the parent hashphashof the blocki+1 - otherwise the
last_block_hashin the tip certificate.
An ICRC-3 compliant Block
- MUST be a
Valueof variantMap - MUST contain a field
phash: Blobwhich is the hash of its parent if it has a parent block - SHOULD contain a field
btype: Stringwhich 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
Each standard that adheres to ICRC-3 MUST define the list of block schemas that it introduces. Each block schema MUST:
- extend the Generic Block Schema
- 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
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 / "_" / "-")
For instance, 1xfer is the identifier of the ICRC-1 transfer operation.
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.
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.
A generic ICRC-1 or ICRC-2 Block:
- it MUST contain a field
ts: Natwhich is the timestamp of when the block was added to the Ledger - if the operation requires a fee and if the
txfield doesn't specify the fee then it MUST contain a fieldfee: Natwhich specifies the fee payed to add this block to the Ledger - its field
tx- CAN contain a field
op: Stringthat uniquely defines the type of operation - MUST contain a field
amt: Natthat represents the amount - MUST contain the
fee: Natfield 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-levelfeeis set. - CAN contain the
memo: Blobfield if specified by the user - CAN contain the
ts: Natfield if the user sets thecreated_at_timefield in the request.
- CAN contain a field
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 btypes 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.
ICRC-1 Account is represented as an Array containing the owner bytes and optionally the subaccount bytes.
- the
btypefield MUST be"1burn"ortx.opfield MUST be"burn" - it MUST contain a field
tx.from: Account
Example with btype:
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"
}};
}}};
}};
Example without btype:
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"
}};
}}};
}};
- the
btypefield MUST be"1mint"or thetx.opfield MUST be"mint" - it MUST contain a field
tx.to: Account
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" };
}}};
}}};
}};
Example without btype:
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" };
}}};
}}};
}};
- the
btypefield MUST be"2xfer"foricrc2_transfer_fromblocks"1xfer"foricrc1_transferblocks
- if
btypeis not set thentx.opfield MUST be"xfer" - it MUST contain a field
tx.from: Account - it MUST contain a field
tx.to: Account - it CAN contain a field
tx.spender: Account
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" };
}}};
}}};
}};
Example without btype:
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" };
}}};
}}};
}};
- the
btypefield MUST be"2approve"ortx.opfield MUST be"approve" - it MUST contain a field
tx.from: Account - it MUST contain a field
tx.spender: Account - it CAN contain a field
tx.expected_allowance: Natif set by the user - it CAN contain a field
tx.expires_at: Natif set by the user
Example with btype:
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" };
}}};
}}};
}}};
Example without btype:
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" };
}}};
}}};
}}};
type Value = variant {
Blob : blob;
Text : text;
Nat : nat; // do we need this or can we just use Int?
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;
};
// 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;
};
service : {
icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query;
};