Add blockchain smart contract lab

This commit is contained in:
juvdiaz 2026-06-29 19:15:09 -06:00
parent bedb845270
commit 945b2a09a7
10 changed files with 203 additions and 0 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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 "$@"
;;

44
labs/blockchain/README.md Normal file
View File

@ -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.

View File

@ -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 = "./" }
]

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

37
scripts/blockchain-test Executable file
View File

@ -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 "$@"