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.