Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: 🚀 Build and publish Docker image

on:
push:
tags:
- 'moc-*'
- 'latest'

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Log in to GitHub Container Registry
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin

- name: Build Docker image
run: docker compose build base

- name: Push Docker image
run: docker compose push base
121 changes: 121 additions & 0 deletions .github/workflows/verifyV3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Verify Black Hole Canister V3

on:
- push
- pull_request
- workflow_dispatch

jobs:
build-and-test:
runs-on: ubuntu-latest

# This sets the default working directory for ALL run steps in this job
defaults:
run:
working-directory: blackholes/v3

steps:
# Checkout happens from the repo root (this is a "uses" step, so the default doesn't affect it)
- name: Check out repository
uses: actions/checkout@v3

- name: Set up Docker Buildx (optional, but recommended)
uses: docker/setup-buildx-action@v2

- name: Build base image
run: |
docker compose build base

- name: Build wasm image
run: |
docker compose build wasm

- name: Run build script inside wasm container and extract hash
id: build_wasm
run: |
# Run docker compose and store the entire output in a variable
OUTPUT=$(docker compose run --rm wasm)

echo "===== Docker Compose Output ====="
echo "$OUTPUT"
echo "================================="

# 2) Look for the line with "out/out_Linux_x86_64.wasm"
# e.g. "79b15176dc613860f35867828f40e7d6... out/out_Linux_x86_64.wasm"
HASH_LINE=$(echo "$OUTPUT" | grep 'out/out_Linux_x86_64.wasm' || true)

if [ -z "$HASH_LINE" ]; then
echo "No line with out/out_Linux_x86_64.wasm found!"
exit 1
fi

# 3) Extract the hash (first space-delimited token)
DOCKER_HASH=$(echo "$HASH_LINE" | awk '{print $1}')
echo "docker_hash=$DOCKER_HASH" >> $GITHUB_OUTPUT
- name: Print build hash
run: |
echo "Docker hash: ${{ steps.build_wasm.outputs.docker_hash }}"

- name: Install dfx
uses: dfinity/setup-dfx@main
with:
dfx-version: 0.26.0-beta.1

- name: Import the buildtest identity
run: |
dfx identity import buildtest ./build-test.pem --disable-encryption --quiet

- name: Gather canister info
id: gather_info
run: |
# 1) Set the environment variable to disable the plaintext warning
export DFX_WARNING=-mainnet_plaintext_identity

# 2) Query dfx for info
CANISTER_ID=$(dfx canister --ic id blackhole)
INFO=$(dfx canister --ic info blackhole)

echo "===== dfx canister --ic info blackhole ====="
echo "$INFO"
echo "========================================"

# 3) Extract the "Controllers:" line (may be empty if 0 controllers)
CONTROLLERS_LINE=$(echo "$INFO" | grep '^Controllers:' || true)
# Remove "Controllers:" prefix + any trailing spaces to get the controller principals
controllers_str=$(echo "$CONTROLLERS_LINE" | sed -E 's/^Controllers:\s*//')

# 4) Extract the "Module hash:" line, then parse out just the hex string
MODULE_HASH_LINE=$(echo "$INFO" | grep '^Module hash:' || true)
MODULE_HASH=$(echo "$MODULE_HASH_LINE" | sed -E 's/^Module hash:\s*0x([A-Za-z0-9]+)/\1/')

# 5) Expose these values as step outputs
echo "controllers_str=$controllers_str" >> $GITHUB_OUTPUT
echo "module_hash=$MODULE_HASH" >> $GITHUB_OUTPUT
echo "canister_id=$CANISTER_ID" >> $GITHUB_OUTPUT

- name: Compare hashes
run: |
DOCKER_HASH="${{ steps.build_wasm.outputs.docker_hash }}"
MODULE_HASH="${{ steps.gather_info.outputs.module_hash }}"

echo "Docker hash: $DOCKER_HASH"
echo "Canister hash: $MODULE_HASH"

if [ "$DOCKER_HASH" = "$MODULE_HASH" ]; then
echo "✓ SUCCESS: Hashes match for canister ${{ steps.gather_info.outputs.canister_id }}."
echo "View this at https://dashboard.internetcomputer.org/canister/${{ steps.gather_info.outputs.canister_id }}"
else
echo "✘ ERROR: Hash mismatch!"
exit 1
fi

- name: Ensure zero controllers
run: |
if [ -n "${{ steps.gather_info.outputs.controllers_str }}" ]; then
echo "✘ ERROR: Found controllers, but expected none."
echo "✘ Controllers: ${{ steps.gather_info.outputs.controllers_str }}"
exit 1
else
echo "✓ No controllers found for canister ${{ steps.gather_info.outputs.canister_id }}."
echo "View this at https://dashboard.internetcomputer.org/canister/${{ steps.gather_info.outputs.canister_id }}"
fi
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# dfx temporary files
.dfx/
.vessel
.mops

# generated files
src/declarations/
Expand Down
23 changes: 23 additions & 0 deletions blackholes/v3/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
ARG IMAGE
FROM --platform=linux/amd64 ${IMAGE}

WORKDIR /project

COPY mops.toml ./

# Let mops-cli install the dependencies defined in mops.toml and create
# mops.lock.
# Note: We trick mops-cli into not downloading binaries and not compiling
# anything. We also make it use the moc version from the base image.
RUN mkdir -p ~/.mops/bin \
&& ln -s /usr/local/bin/moc ~/.mops/bin/moc \
&& touch ~/.mops/bin/mo-fmt \
&& echo "actor {}" >tmp.mo \
&& mops-cli build tmp.mo -- --check \
&& rm -r tmp.mo target/tmp

COPY src /project/src/
COPY di[d] /project/did/
COPY build.sh /project

CMD ["/bin/bash"]
37 changes: 37 additions & 0 deletions blackholes/v3/Dockerfile.base
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
ARG PLATFORM=linux/amd64
FROM --platform=${PLATFORM} alpine:latest AS build

RUN apk add --no-cache curl ca-certificates tar \
&& update-ca-certificates

RUN mkdir -p /install/bin

# Install ic-wasm
ARG IC_WASM_VERSION
RUN curl -L https://github.com/research-ag/ic-wasm/releases/download/${IC_WASM_VERSION}/ic-wasm-x86_64-unknown-linux-musl.tar.gz -o ic-wasm.tgz \
&& tar xzf ic-wasm.tgz \
&& install ic-wasm /install/bin

# Install mops-cli
ARG MOPS_CLI_VERSION
RUN curl -L https://github.com/dfinity/mops-cli/releases/download/${MOPS_CLI_VERSION}/mops-cli-linux64 -o mops-cli \
&& install mops-cli /install/bin

# Install moc
ARG MOC_VERSION
RUN if dpkg --compare-versions "${MOC_VERSION}" lt "0.9.5"; then \
curl -L https://github.com/dfinity/motoko/releases/download/${MOC_VERSION}/motoko-linux64-${MOC_VERSION}.tar.gz -o motoko.tgz; \
else \
curl -L https://github.com/dfinity/motoko/releases/download/${MOC_VERSION}/motoko-Linux-x86_64-${MOC_VERSION}.tar.gz -o motoko.tgz; \
fi \
&& tar xzf motoko.tgz \
&& install moc /install/bin

# If dpkg is not available then use this line above:
# RUN if [ "$(printf '%s\n' "${MOC_VERSION}" "0.9.4" | sort -V | head -n 1)" = "${MOC_VERSION}" ]; then \



FROM --platform=${PLATFORM} alpine:latest
RUN apk add bash
COPY --from=build /install/bin/* /usr/local/bin/
28 changes: 28 additions & 0 deletions blackholes/v3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Introduction

- This repository contains the [source code](blackhole.mo) for canister `cpbhu-5iaaa-aaaad-aalta-cai`, version 3 (V3) of the the CycleOps balance checker canister
- It uses the [Reproducible Build Template from research-ag](https://github.com/research-ag/motoko-build-template), allowing anyone to build this canister and verify its module hash locally.
- You can also run the V3 GitHub action attached to this repository in order to verify that `cpbhu-5iaaa-aaaad-aalta-cai` is:
- blackholed, with 0 controllers
- running the same wasm binary generated from ([./blackhole.mo](blackhole.mo))

<br/>

# Verification of the Blackholed Balance Checker canister

## Easy Mode: Use a GH Action

This will only take a few clicks!

1. Fork this repository.
2. Navigate to the actions tab of your new repo.
3. Select the "Verify Black Hole Canister V3" action on the left.
4. Click the "Run workflow" button to dispatch the action.

You can view the output of this action to confirm that 1) the canister has no controllers, 2) the mainnet wasm matches the contents of this repository. The final step to be 100% confident that this canister can do no harm is to audit the source code of this repo.

## Alternative: Verify on your local machine

This repository uses the [Reproducible Build Motoko Template Standard from research-ag](https://github.com/research-ag/motoko-build-template). In this folder, follow the instructions listed there in order to spin up docker and build the [canister code](./src/blackhole.mo) with the correct version of dfx and the motoko compiler inside of a linux x86_64 environment.

The resulting hash should match `dfcf37d4ee18bfe77a12197feaf4686f160d717383f0cf525f9d30e60fec204d`
5 changes: 5 additions & 0 deletions blackholes/v3/build-test.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIMDTUL1yDOa3xo5Wf4bUUxKPUamRA0GWEByV7zLUdUROoAcGBSuBBAAK
oUQDQgAEbSRh8i80GyA2h+8q/PJ0N6HFQnqKCp4DYJPOmUQXdolnhqQ/IRBxm0u8
Rzx1AqqPpzOG2sp7AGMxwF1lciKpAw==
-----END EC PRIVATE KEY-----
20 changes: 20 additions & 0 deletions blackholes/v3/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

MOC_GC_FLAGS="" ## place any additional flags like compacting-gc, incremental-gc here
MOC_FLAGS="$MOC_GC_FLAGS -no-check-ir --release --public-metadata candid:service --public-metadata candid:args"
OUT=out/out_$(uname -s)_$(uname -m).wasm
mops-cli build --lock --name out src/blackhole.mo -- $MOC_FLAGS
cp target/out/out.wasm $OUT
ic-wasm $OUT -o $OUT shrink
if [ -f did/service.did ]; then
echo "Adding service.did to metadata section."
ic-wasm $OUT -o $OUT metadata candid:service -f did/service.did -v public
else
echo "service.did not found. Skipping metadata update."
fi
if [ "$compress" == "yes" ] || [ "$compress" == "y" ]; then
gzip -nf $OUT
sha256sum $OUT.gz
else
sha256sum $OUT
fi
5 changes: 5 additions & 0 deletions blackholes/v3/canister_ids.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"blackhole": {
"ic": "cpbhu-5iaaa-aaaad-aalta-cai"
}
}
21 changes: 21 additions & 0 deletions blackholes/v3/dfx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"canisters": {
"blackhole": {
"main": "./src/blackhole.mo",
"type": "motoko"
}
},
"defaults": {
"build": {
"args": "-no-check-ir",
"packtool": "mops sources"
}
},
"dfx": "0.26.0-beta.1",
"networks": {
"local": {
"bind": "127.0.0.1:8089"
}
},
"version": 1
}
74 changes: 74 additions & 0 deletions blackholes/v3/did/service.did
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
type StatusChecker =
service {
canisterStatus: (principal) -> (CanisterStatusResult);
/// checks the canister status of itself or anything it is the controller of
canisterStatuses: (vec principal, nat) -> (Result);
};
type Result =
variant {
err: text;
ok: vec CanisterStatusResult;
};
type QueryStats =
record {
num_calls_total: nat;
num_instructions_total: nat;
request_payload_bytes_total: nat;
response_payload_bytes_total: nat;
};
type MemoryMetrics =
record {
canister_history_size: nat;
custom_sections_size: nat;
global_memory_size: nat;
snapshots_size: nat;
stable_memory_size: nat;
wasm_binary_size: nat;
wasm_chunk_store_size: nat;
wasm_memory_size: nat;
};
type DefiniteCanisterSettings =
record {
compute_allocation: nat;
controllers: vec principal;
freezing_threshold: nat;
log_visibility:
variant {
allowed_viewers: vec principal;
controllers;
public;
};
memory_allocation: nat;
reserved_cycles_limit: nat;
wasm_memory_limit: nat;
};
type CanisterStatusResult =
variant {
err: text;
ok: CanisterStatus;
};
type CanisterStatus =
record {
cycles: nat;
idle_cycles_burned_per_day: nat;
memory_metrics: MemoryMetrics;
memory_size: nat;
module_hash: opt blob;
query_stats: QueryStats;
reserved_cycles: nat;
settings: DefiniteCanisterSettings;
status: variant {
running;
stopped;
stopping;
};
};
/// CycleOps Status Checker V3 Implementation
/// The canister status checker actor responsible for monitoring the cycles balances of customer-owned canisters
/// CycleOps spins up a blackholed version of this canister.
///
/// The caller is the original creator of this service. This cannot change throughout the lifecycle of the canister
/// as it can only be created once. Any subsequent upgrades by other controllers cannot change this field.
/// The access control implemented in this canister's public APIs means that the CycleOps Service is the only
/// caller with the ability to invoke the public methods of this canister.
service : (principal) -> StatusChecker
Loading