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
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 theleb128
encoding of the number - the hash of an
Int
is the hash of thesleb128
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
.
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 asleb128
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.
Validation of block i
is done by checking the block hash against
- if
i + 1 < len(chain)
then the parent hashphash
of the blocki+1
- otherwise the
last_block_hash
in the tip certificate.
An ICRC-3 compliant Block
- MUST be a
Value
of variantMap
- MUST contain a field
phash: Blob
which is the hash of its parent if it has a parent block - 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
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: Nat
which is the timestamp of when the block was added to the Ledger - if the operation requires a fee and if the
tx
field doesn't specify the fee then it MUST contain a fieldfee: Nat
which specifies the fee payed to add this block to the Ledger - its field
tx
- CAN contain a field
op: String
that uniquely defines the type of operation - 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-levelfee
is set. - CAN contain the
memo: Blob
field if specified by the user - CAN contain the
ts: Nat
field if the user sets thecreated_at_time
field 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 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.
ICRC-1 Account is represented as an Array
containing the owner
bytes and optionally the subaccount bytes.
- the
btype
field MUST be"1burn"
ortx.op
field 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
btype
field MUST be"1mint"
or thetx.op
field 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
btype
field MUST be"2xfer"
foricrc2_transfer_from
blocks"1xfer"
foricrc1_transfer
blocks
- if
btype
is not set thentx.op
field 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
btype
field MUST be"2approve"
ortx.op
field 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: Nat
if set by the user - it CAN contain a field
tx.expires_at: Nat
if 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;
};