A simple experiment with collectible canisters.
Examples
A mock collectible card game:
These were built with the following standalone C program:
#define IMPORT(m,n) __attribute__((import_module(m))) __attribute__((import_name(n))); #define EXPORT(n) asm(n) __attribute__((visibility("default"))) typedef unsigned u32; typedef unsigned char u8; u32 caller_size(void) IMPORT("ic0", "msg_caller_size"); void caller_copy(void*, u32, u32) IMPORT("ic0", "msg_caller_copy"); void reply_append(void*, u32) IMPORT("ic0", "msg_reply_data_append"); void reply(void) IMPORT("ic0", "msg_reply"); void reject(void*, u32) IMPORT("ic0", "msg_reject"); u32 arg_size(void) IMPORT("ic0", "msg_arg_data_size"); void arg_copy(void *, u32, u32) IMPORT("ic0", "msg_arg_data_copy"); #define REJECT(s) return reject(s, sizeof(s) - 1); const u8 artist[] = {0xc8,0x4d,0x5f,0x5c,0x86,0x27,0x18,0x80,0x7e,0x4f,0x6f,0x93,0x4d,0xe0,0x61,0x31,0x5e,0xc4,0xbb,0x84,0x67,0xe2,0x8f,0x03,0xb7,0xaa,0x1d,0x7e,0x02}; u8 *msg; u32 msg_size; extern u32 __heap_base; void* malloc(unsigned long n) { static u32 bump = (u32) &__heap_base; return (void *) ((bump += n) - n); } void publish() EXPORT("canister_update publish"); void publish() { if (msg_size) REJECT("already published"); u8 buf[128]; u32 n = caller_size(); if (n != sizeof(artist)) REJECT("bad caller size"); caller_copy(buf, 0, n); for (u32 i = 0; i < n; i++) if (buf[i] != artist[i]) REJECT("bad caller"); msg_size = arg_size(); msg = malloc(msg_size); arg_copy(msg, 0, msg_size); reply_append((u8[]) {0x44,0x49,0x44,0x4c,0x00,0x00}, 6); reply(); } void http_request() EXPORT("canister_query http_request"); void http_request() { u8 hdr[] = {0x44,0x49,0x44,0x4c,0x03,0x6c,0x03,0xa2,0xf5,0xed,0x88,0x04,0x01,0xc6,0xa4,0xa1,0x98,0x06,0x02,0x9a,0xa1,0xb2,0xf9,0x0c,0x7a,0x6d,0x7b,0x6d,0x6f,0x01,0x00}; reply_append(hdr, sizeof(hdr)); u32 n = msg_size; do { u8 single[1]; *single = (n & 127) | ((n > 127) << 7); reply_append(single, 1); n >>= 7; } while(n); reply_append(msg, msg_size); u8 ftr[] = {0x00,0xc8,0x00}; reply_append(ftr, sizeof(ftr)); reply(); }
We begin with an empty msg
buffer.
The publish
method expects raw data that is not necessarily in Candid binary
format. If the caller matches the blob stored in artist
and the msg
buffer
is empty, then the msg
is updated with the given data.
The http_request
method returns the msg
buffer.
Hence when deployed, nobody except artist
can change the msg
buffer, and the
artist
may only supply nontrivial data once. Furthermore, anyone who checks
the module hash is convinced that the canister’s contents were put there by
artist
.
As can be seen in the source, our artist
has the raw principal ID:
c84d5f5c862718807e4f6f934de061315ec4bb8467e28f03b7aa1d7e02
though we often prepend a checksum and write it in Base32 with hyphens:
bxxti-j6ijv-pvzbr-hdcah-4t3ps-ng6ay-jrl3c-lxbdh-4khqh-n5kdv-7ae
The controller of such a canister represents the current owner. As with other collectibles, the owner is free to destroy or deface it, or transfer ownership.
Thus we take advantage of the Internet Computer’s native features to get free bookkeeping. There’s no need to maintain our own global map of who owns what, or who created what. Instead we use built-in infrastructure: the controller field and the module hash.
The only cost is paid by the controller, who is responsible for providing the collectible canister enough cycles to survive. This ought to be cheap, as there is only one trivial update call and one query call.
Module Hash
We build our canister with Clang 14:
$ clang -O2 -c --target=wasm32 -Wall bxxti.c $ wasm-ld --export-dynamic --import-undefined --no-entry --global-base=0 --initial-memory=41943040 bxxti.o -o bxxti.wasm
The SHA256 hash of the resulting binary is:
b27aa431d47a8920b221bc608b0e3c1f66db0fcf7a127316dc54e36b02709f70
We can check hashes with something like:
$ dfx canister --network ic info qdaz7-qqaaa-aaaae-aaipa-cai
We create a minimal dfx.json
:
{"canisters": { "bxxti":{"type":"custom","candid":"did.not","wasm":"bxxti.wasm","build":""} }}
Then deploy:
$ touch did.not $ dfx canister --network ic deploy bxxti
Whether or not they control the canister, the artist
can publish a file with:
$ dfx canister --network ic call bxxti publish --type raw `xxd -p FILE | tr -d '\n'`
This can only be done once. To correct a mistake, we must redeploy the canister.
Hex Dump
The bxxti.wasm
binary:
0061736D0100000001130460027F7F006000017F60037F7F7F00600000029A0107036963300A 6D73675F72656A6563740000036963300F6D73675F63616C6C65725F73697A65000103696330 0F6D73675F63616C6C65725F636F7079000203696330116D73675F6172675F646174615F7369 7A65000103696330116D73675F6172675F646174615F636F7079000203696330156D73675F72 65706C795F646174615F617070656E64000003696330096D73675F7265706C79000303030203 030504010080050608017F01418081040B074203066D656D6F727902001763616E6973746572 5F757064617465207075626C69736800071B63616E69737465725F717565727920687474705F 7265717565737400080ADA0402E30201037F2380808080004190016B22002480808080000240 024041002802F880808000450D0041B88080800041111080808080000C010B02401081808080 00411D460D0041A880808000410F1080808080000C010B41002101200041106A4100411D1082 80808000024002400340200041106A20016A22022D000020014180808080006A2D0000470D01 2001411C460D02200241016A2D000020014181808080006A2D0000470D01200241026A2D0000 20014182808080006A2D0000470D01200241036A2D000020014183808080006A2D0000470D01 200141046A21010C000B0B419D80808000410A1080808080000C010B41001083808080002201 3602F880808000410041002802F48080800022023602FC808080004100200220016A3602F480 808000200241002001108480808000200041003B000E200041C49291E20436000A2000410A6A 41061085808080001086808080000B20004190016A2480808080000BF20101037F2380808080 0041306B2200248080808000200041276A41002900E780808000370000200041206A41002903 E080808000370300200041002903D880808000370318200041002903D0808080003703102000 41106A411F10858080800041002802F880808000210103402000200141FF004B220241077420 0141FF0071723A000C2000410C6A41011085808080002001410776210120020D000B41002802 FC8080800041002802F8808080001085808080002000410E6A41002D00F1808080003A000020 0041002F00EF808080003B010C2000410C6A4103108580808000108680808000200041306A24 80808080000B0B8201020041000B72C84D5F5C862718807E4F6F934DE061315EC4BB8467E28F 03B7AA1D7E026261642063616C6C6572006261642063616C6C65722073697A6500616C726561 6479207075626C6973686564000000000000004449444C036C03A2F5ED880401C6A4A1980602 9AA1B2F90C7A6D7B6D6F010000C8000041F4000B048000010000B101046E616D650182010900 0672656A656374010B63616C6C65725F73697A65020B63616C6C65725F636F70790308617267 5F73697A6504086172675F636F7079050C7265706C795F617070656E6406057265706C790717 63616E69737465725F757064617465207075626C697368081B63616E69737465725F71756572 7920687474705F72657175657374071201000F5F5F737461636B5F706F696E74657209110200 072E726F6461746101052E6461746100360970726F647563657273010C70726F636573736564 2D6279010C5562756E747520636C616E670F31342E302E302D317562756E747531
Future Work
Relying on the module hash requires robust reproducible builds, a problem which grows thornier as we add dependencies.
Perhaps for such a simple canister, we may want to write wasm directly, or use a simple predictable compiler, ideally one that runs on the Internet Computer. (I have written such a compiler in the past, but I need to spruce it up before showing it off.)
On the other hand, our canister may not stay simple for long. Certified data might be desirable, which would require a SHA-256 routine.
Instead of hard-coding the artist
principal, we could simply record the
caller’s principal the first time that publish
is called, and add a query
method that returns it so anyone can see who created the content. Naturally,
anyone could then publish to the canister, but it seems redeploying would be
enough to stop rogue publishers; if not, we could add a canister_init
function that takes the principal of the artist we really want.
We may want a way for an artist to supply a digital signature of the canister
ID and its contents. While the module hash already implies a certain principal
made a call to publish
, the security of a signature depends only on
mathematics, rather than the Internet Computer.
The burden is on the collector to find the source of the canister and build it to get the same hash. This task could be made friendlier with a quine-like feature: a query method which returns the source of the canister, ideally with build instructions.
The collector may want to record a http_request
request and certified
response from the Internet Computer, because this includes a timestamp. If
somebody later copies the contents of the canister and tries to pass it off as
their own original work, then the signed timestamp can help expose this
duplicity. Or if the private key of the artist is leaked, then the collector
has proof that their canister’s contents were published before the security
breach.
Building a real CCG with this design could be challenging. Although the gameplay canister can check the module hash is correct, there is a race: a cheater could upgrade their canister just after the hash check but before their canister is used to determine an outcome in the game. (Of course, such an upgrade destroys the original content and would need the artist’s help to to restore it.) It might help if the system supported a kind of call that only succeeds if the module hash has a given value.