diff --git a/standards/ICRC-3/HASHINGVALUES.md b/standards/ICRC-3/HASHINGVALUES.md new file mode 100644 index 00000000..52489cf2 --- /dev/null +++ b/standards/ICRC-3/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/standards/ICRC-3/ICRC-3.did b/standards/ICRC-3/ICRC-3.did new file mode 100644 index 00000000..70c5fc39 --- /dev/null +++ b/standards/ICRC-3/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/standards/ICRC-3/README.md b/standards/ICRC-3/README.md new file mode 100644 index 00000000..c06f0532 --- /dev/null +++ b/standards/ICRC-3/README.md @@ -0,0 +1,401 @@ +# `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 transactions 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` represent 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 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) +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. + +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: 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: + +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 +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. + +## 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. + +## [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. + +A generic ICRC-1 or ICRC-2 Block: + +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 + 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. + +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. + +### Burn Block Schema + +1. the `btype` field MUST be `"1burn"` or `tx.op` field MUST be `"burn"` +2. 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" + }}; + }}}; +}}; +``` + +#### Mint Block Schema + +1. the `btype` field MUST be `"1mint"` or the `tx.op` field MUST be `"mint"` +2. 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" }; + }}}; + }}}; +}}; +``` + +#### 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 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" }; + }}}; + }}}; +}}; +``` + +#### Approve Block Schema + +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`: +``` +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" }; + }}}; + }}}; +}}}; +``` + +## Specification + +### `icrc3_get_blocks` + +``` +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; +}; +``` + +### `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; +}; +```