From 945b2a09a725b62a0306d6b9bcf624c65da407d5 Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 29 Jun 2026 19:15:09 -0600 Subject: [PATCH] Add blockchain smart contract lab --- README.md | 5 +++ README.md.tmpl | 5 +++ docs/jeannie.1.md | 4 ++ jeannie | 8 ++++ labs/blockchain/README.md | 44 ++++++++++++++++++++++ labs/blockchain/foundry.toml | 12 ++++++ labs/blockchain/script/DeployCounter.s.sol | 15 ++++++++ labs/blockchain/src/Counter.sol | 32 ++++++++++++++++ labs/blockchain/test/Counter.t.sol | 41 ++++++++++++++++++++ scripts/blockchain-test | 37 ++++++++++++++++++ 10 files changed, 203 insertions(+) create mode 100644 labs/blockchain/README.md create mode 100644 labs/blockchain/foundry.toml create mode 100644 labs/blockchain/script/DeployCounter.s.sol create mode 100644 labs/blockchain/src/Counter.sol create mode 100644 labs/blockchain/test/Counter.t.sol create mode 100755 scripts/blockchain-test diff --git a/README.md b/README.md index a68e171..fb6f851 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ Run a read-only health snapshot from the Debian server with: ./jeannie resource-budget ./jeannie artifact-cache status ./jeannie blockchain-devnet status +./jeannie blockchain-test ./jeannie golden-ledger check ./jeannie route-inventory ./jeannie workers list @@ -374,6 +375,10 @@ cache. Use `artifact-cache instructions` for client configuration snippets. `infra/blockchain-devnet`. It is for contract, wallet, RPC, and observability learning only; never use real seed phrases or mainnet keys. +`blockchain-test` runs the Foundry tests in `labs/blockchain`. The lab starts +with a simple `Counter` contract and ownership tests so there is a known-good +baseline before practicing vulnerable contracts. + `golden-ledger` shows or validates `infra/pimox/golden-image-ledger.yml`, the reviewable record of template VMID, storage, OS release, Kubernetes pins, runtime versions, and build metadata for Pimox worker golden images. diff --git a/README.md.tmpl b/README.md.tmpl index b38602b..926b31d 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -284,6 +284,7 @@ Run a read-only health snapshot from the Debian server with: ./{{ main_script }} resource-budget ./{{ main_script }} artifact-cache status ./{{ main_script }} blockchain-devnet status +./{{ main_script }} blockchain-test ./{{ main_script }} golden-ledger check ./{{ main_script }} route-inventory ./{{ main_script }} workers list @@ -374,6 +375,10 @@ cache. Use `artifact-cache instructions` for client configuration snippets. `infra/blockchain-devnet`. It is for contract, wallet, RPC, and observability learning only; never use real seed phrases or mainnet keys. +`blockchain-test` runs the Foundry tests in `labs/blockchain`. The lab starts +with a simple `Counter` contract and ownership tests so there is a known-good +baseline before practicing vulnerable contracts. + `golden-ledger` shows or validates `infra/pimox/golden-image-ledger.yml`, the reviewable record of template VMID, storage, OS release, Kubernetes pins, runtime versions, and build metadata for Pimox worker golden images. diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 1fdc86e..3f97f12 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -127,6 +127,10 @@ Docker Hub pull-through image caching. `infra/blockchain-devnet`. This is for local learning only; never use real seed phrases or mainnet keys. +`blockchain-test [forge args...]` +: Run the Foundry smart contract tests in `labs/blockchain`. Extra arguments are +passed through to `forge test`. + `golden-ledger {show|check}` : Show or validate the Pimox golden image version ledger. diff --git a/jeannie b/jeannie index 2629612..22007d5 100755 --- a/jeannie +++ b/jeannie @@ -6012,6 +6012,10 @@ blockchain_devnet() { "${REPO_ROOT}/scripts/blockchain-devnet" "${@:2}" } +blockchain_test() { + "${REPO_ROOT}/scripts/blockchain-test" "${@:2}" +} + golden_ledger() { "${REPO_ROOT}/scripts/golden-ledger" "${@:2}" } @@ -6080,6 +6084,7 @@ Build And Bootstrap Manage optional Debian artifact caches. blockchain-devnet {status|up|down|logs|rpc} Manage the local Ethereum Anvil devnet. + blockchain-test [forge args...] Run Foundry tests for labs/blockchain. golden-ledger {show|check} Show or validate Pimox golden image versions. Cluster Lifecycle @@ -6218,6 +6223,9 @@ case "${1:-}" in blockchain-devnet) blockchain_devnet "$@" ;; + blockchain-test) + blockchain_test "$@" + ;; golden-ledger) golden_ledger "$@" ;; diff --git a/labs/blockchain/README.md b/labs/blockchain/README.md new file mode 100644 index 0000000..eb344b1 --- /dev/null +++ b/labs/blockchain/README.md @@ -0,0 +1,44 @@ +# Blockchain Contract Lab + +This is the local smart contract workspace for the homelab. It uses Foundry +because it is fast, test-oriented, and works well with a local Anvil devnet. + +Never put real private keys, seed phrases, or funded wallet data in this repo. + +## Layout + +```text +labs/blockchain/ + foundry.toml + src/ + test/ + script/ +``` + +## Test + +```bash +./jeannie blockchain-test +``` + +The wrapper uses a local `forge` binary when available, otherwise it runs the +Foundry Docker image with this directory mounted. + +## Deploy To The Local Devnet + +Start the local devnet: + +```bash +./jeannie blockchain-devnet up +``` + +Deploy using one of Anvil's generated dev private keys: + +```bash +cd labs/blockchain +PRIVATE_KEY=0x... forge script script/DeployCounter.s.sol \ + --rpc-url http://127.0.0.1:8545 \ + --broadcast +``` + +Use only Anvil dev keys. Do not paste a real wallet key into a shell. diff --git a/labs/blockchain/foundry.toml b/labs/blockchain/foundry.toml new file mode 100644 index 0000000..5c6c62b --- /dev/null +++ b/labs/blockchain/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +test = "test" +script = "script" +solc_version = "0.8.26" +optimizer = true +optimizer_runs = 200 +fs_permissions = [ + { access = "read", path = "./" } +] diff --git a/labs/blockchain/script/DeployCounter.s.sol b/labs/blockchain/script/DeployCounter.s.sol new file mode 100644 index 0000000..d0d4a80 --- /dev/null +++ b/labs/blockchain/script/DeployCounter.s.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Script} from "forge-std/Script.sol"; +import {Counter} from "../src/Counter.sol"; + +contract DeployCounter is Script { + function run() external returns (Counter counter) { + uint256 deployerKey = vm.envUint("PRIVATE_KEY"); + + vm.startBroadcast(deployerKey); + counter = new Counter(0); + vm.stopBroadcast(); + } +} diff --git a/labs/blockchain/src/Counter.sol b/labs/blockchain/src/Counter.sol new file mode 100644 index 0000000..ca6b18e --- /dev/null +++ b/labs/blockchain/src/Counter.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +contract Counter { + uint256 public number; + address public owner; + + error NotOwner(); + + event NumberChanged(uint256 previousNumber, uint256 newNumber); + + constructor(uint256 initialNumber) { + owner = msg.sender; + number = initialNumber; + } + + function setNumber(uint256 newNumber) external { + if (msg.sender != owner) { + revert NotOwner(); + } + + uint256 previousNumber = number; + number = newNumber; + emit NumberChanged(previousNumber, newNumber); + } + + function increment() external { + uint256 previousNumber = number; + number++; + emit NumberChanged(previousNumber, number); + } +} diff --git a/labs/blockchain/test/Counter.t.sol b/labs/blockchain/test/Counter.t.sol new file mode 100644 index 0000000..019df10 --- /dev/null +++ b/labs/blockchain/test/Counter.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter private counter; + address private owner = address(0xA11CE); + address private stranger = address(0xB0B); + + function setUp() public { + vm.prank(owner); + counter = new Counter(41); + } + + function testInitialNumberAndOwner() public view { + assertEq(counter.number(), 41); + assertEq(counter.owner(), owner); + } + + function testOwnerCanSetNumber() public { + vm.prank(owner); + counter.setNumber(100); + + assertEq(counter.number(), 100); + } + + function testNonOwnerCannotSetNumber() public { + vm.prank(stranger); + vm.expectRevert(Counter.NotOwner.selector); + counter.setNumber(100); + } + + function testAnyoneCanIncrement() public { + vm.prank(stranger); + counter.increment(); + + assertEq(counter.number(), 42); + } +} diff --git a/scripts/blockchain-test b/scripts/blockchain-test new file mode 100755 index 0000000..467ff8b --- /dev/null +++ b/scripts/blockchain-test @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LAB_DIR="${REPO_ROOT}/labs/blockchain" +FOUNDRY_IMAGE="${FOUNDRY_IMAGE:-ghcr.io/foundry-rs/foundry:latest}" + +usage() { + cat <<'EOF' +Usage: ./jeannie blockchain-test [forge args...] + +Run Foundry tests for labs/blockchain. Extra arguments are passed to forge test. +EOF +} + +case "${1:-}" in + -h|--help|help) + usage + exit 0 + ;; +esac + +if command -v forge >/dev/null 2>&1; then + cd "$LAB_DIR" + exec forge test "$@" +fi + +if ! command -v docker >/dev/null 2>&1; then + echo "forge or docker is required for blockchain-test." >&2 + exit 1 +fi + +exec docker run --rm \ + -v "${LAB_DIR}:/workspace" \ + -w /workspace \ + "$FOUNDRY_IMAGE" \ + forge test "$@"