From c41882085906d724aeb9b1e04374ead45ec30d40 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Sat, 11 Jul 2026 16:25:45 +0300 Subject: [PATCH 1/2] feat(decdn_node): expose slash-appeal + scan-floor config knobs (ADR 028) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync the decdn_node role to upstream operator slash detection + appeal (decdn PR #1089, ADR 028). Add two optional [blockchain] knobs to the rendered node.toml, both emitted only when set (mirroring the existing relay_url optional-emit pattern): - decdn_slash_appeal_address — the SlashAppeal contract, target of `decdn appeal slash`. Consumed only by that CLI; the daemon accepts but ignores the key. Validated (0x+40-hex, non-zero) via a when-guarded assert only when an operator sets it, matching the fail-loud posture of the required contract addresses. - decdn_slash_judge_from_block — scan floor for the daemon's new SlashJudge.Slashed watcher. Bounds the full-chain rescan that otherwise runs on every restart. An always-run assert enforces integer shape so a typo can't be silently coerced to 0 by Jinja's int filter and drop the key without a trace. Contract addresses stay operator-supplied (empty defaults, host_vars) per the no-baked-protocol-facts rule. README + host_vars example document the appeal path. Verified: yamllint, ansible-lint (production profile), template render (unset vs set), live assert execution, galaxy build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../host_vars/decdn-node-1.yml.example | 4 +++ ansible/roles/decdn_node/README.md | 12 ++++++- ansible/roles/decdn_node/defaults/main.yml | 7 ++++ ansible/roles/decdn_node/tasks/main.yml | 32 +++++++++++++++++++ .../roles/decdn_node/templates/node.toml.j2 | 6 ++++ 5 files changed, 60 insertions(+), 1 deletion(-) diff --git a/ansible/inventory/host_vars/decdn-node-1.yml.example b/ansible/inventory/host_vars/decdn-node-1.yml.example index 5af6254..85daf93 100644 --- a/ansible/inventory/host_vars/decdn-node-1.yml.example +++ b/ansible/inventory/host_vars/decdn-node-1.yml.example @@ -19,6 +19,10 @@ decdn_payment_channel_address: "0xREPLACE_PaymentChannel_FROM_DEPLOYMENT" decdn_capacity_bond_address: "0xREPLACE_CapacityBond_FROM_DEPLOYMENT" decdn_slash_judge_address: "0xREPLACE_SlashJudge_FROM_DEPLOYMENT" +# --- Slash appeals (optional; ADR 028) ---------------------------------------- +# decdn_slash_appeal_address: "0xREPLACE_SlashAppeal_FROM_DEPLOYMENT" # for `decdn appeal slash` +# decdn_slash_judge_from_block: 0 # SlashJudge deploy block — set to bound per-restart RPC rescan + # --- Node identity / locale --------------------------------------------------- decdn_region: "US" # ISO 3166-1 alpha-2 of the node's physical location # decdn_relay_url: "" # optional iroh relay for NAT traversal diff --git a/ansible/roles/decdn_node/README.md b/ansible/roles/decdn_node/README.md index 11b5fa7..6b86bac 100644 --- a/ansible/roles/decdn_node/README.md +++ b/ansible/roles/decdn_node/README.md @@ -50,7 +50,12 @@ Per `decdn/adr/019-node-onboarding.md`, a node only serves paid traffic after `decdn_payment_channel_address`, `decdn_capacity_bond_address`, `decdn_slash_judge_address` (all `0x`+40-hex; SlashJudge non-zero), `decdn_region` (ISO 3166-1 alpha-2). Contract addresses/chain-id are protocol -facts — source them from the deployment / an ADR, never guess. See +facts — source them from the deployment / an ADR, never guess. + +Optional (omitted from `node.toml` unless set): `decdn_slash_appeal_address` +(SlashAppeal contract, source from the deployment / ADR 028 — only needed to file +appeals with `decdn appeal slash`) and `decdn_slash_judge_from_block` (SlashJudge +deploy block; bounds the slash-detection watcher's per-restart chain rescan). See `roles/decdn_node/defaults/main.yml` for the full knob list and defaults. ## Network @@ -77,6 +82,11 @@ systemctl status decdn-node journalctl -u decdn-node -e decdn node health # admin RPC (127.0.0.1:9191) decdn node peers + +# If this node is slashed (surfaced via the admin RPC — admin_v1_slashes), file an +# appeal within the ADR 028 window. Needs decdn_slash_appeal_address set in node.toml +# (else pass --slash-appeal-address / DECDN_SLASH_APPEAL_ADDRESS): +decdn appeal slash ``` Upgrades: bump `decdn_node_version` (+ `decdn_node_sha256`) and re-deploy — the diff --git a/ansible/roles/decdn_node/defaults/main.yml b/ansible/roles/decdn_node/defaults/main.yml index 7200e9d..26f7377 100644 --- a/ansible/roles/decdn_node/defaults/main.yml +++ b/ansible/roles/decdn_node/defaults/main.yml @@ -18,6 +18,13 @@ decdn_rpc_url: "" # SENSITIVE; may embed an API key decdn_payment_channel_address: "" decdn_capacity_bond_address: "" decdn_slash_judge_address: "" +# Optional (ADR 028). SlashAppeal contract — only needed to file appeals with +# `decdn appeal slash`; the daemon ignores it. Empty => key omitted from node.toml. +decdn_slash_appeal_address: "" +# Optional. SlashJudge deploy block: the slash-detection watcher rescans from +# here on EVERY daemon start, so 0 (the upstream default) re-scans the full chain +# each restart — RPC-heavy on an established L2. Set to bound restart cost. +decdn_slash_judge_from_block: 0 decdn_region: "" # ISO 3166-1 alpha-2 decdn_chain_id: 421614 # Arbitrum Sepolia (matches decdn-node's --chain-id default) diff --git a/ansible/roles/decdn_node/tasks/main.yml b/ansible/roles/decdn_node/tasks/main.yml index 0359cdc..a8dd300 100644 --- a/ansible/roles/decdn_node/tasks/main.yml +++ b/ansible/roles/decdn_node/tasks/main.yml @@ -32,6 +32,38 @@ vars: decdn_zero_address: "0x0000000000000000000000000000000000000000" +# slash_appeal_address is optional (only `decdn appeal slash` uses it — the daemon +# ignores the key), so it is not in the hard assert above. But if an operator sets +# it, a half-filled/zero value would silently render a bad node.toml, so validate +# it the same fail-loud way as the required addresses when present. +- name: Validate the optional SlashAppeal address when set + ansible.builtin.assert: + that: + - decdn_slash_appeal_address is match('^0x[0-9a-fA-F]{40}$') + - decdn_slash_appeal_address != decdn_zero_address + fail_msg: >- + decdn_slash_appeal_address is set but is not a valid contract address + (0x + 40 hex, not the zero address). Leave it empty to omit it, or set the + deployed SlashAppeal address (source it from the deployment / ADR 028). + vars: + decdn_zero_address: "0x0000000000000000000000000000000000000000" + when: decdn_slash_appeal_address | length > 0 + +# slash_judge_from_block feeds a `| int > 0` gate in node.toml.j2, and Jinja's int +# filter silently coerces any unparseable value to 0 — which drops the key and +# reverts the daemon to a full-chain rescan on every restart, discarding the +# operator's input with no trace in the rendered config. Assert integer shape so a +# typo fails loud at deploy time instead. Runs unconditionally: the default 0 +# passes, so a malformed value can never slip through the coercion silently. +- name: Validate the SlashJudge scan-floor block is a non-negative integer + ansible.builtin.assert: + that: + - decdn_slash_judge_from_block | string is match('^[0-9]+$') + fail_msg: >- + decdn_slash_judge_from_block must be a non-negative integer (the SlashJudge + deploy block). Got "{{ decdn_slash_judge_from_block }}". Leave it 0 to scan + from genesis, or set the deploy block to bound per-restart rescan cost. + # --- User & directories ------------------------------------------------------- - name: Create decdn system group ansible.builtin.group: diff --git a/ansible/roles/decdn_node/templates/node.toml.j2 b/ansible/roles/decdn_node/templates/node.toml.j2 index 488d3cc..8ea898e 100644 --- a/ansible/roles/decdn_node/templates/node.toml.j2 +++ b/ansible/roles/decdn_node/templates/node.toml.j2 @@ -20,6 +20,12 @@ eth_keystore = "{{ decdn_keystore_file }}" payment_channel_address = "{{ decdn_payment_channel_address }}" capacity_bond_address = "{{ decdn_capacity_bond_address }}" slash_judge_address = "{{ decdn_slash_judge_address }}" +{% if decdn_slash_appeal_address | length > 0 %} +slash_appeal_address = "{{ decdn_slash_appeal_address }}" +{% endif %} +{% if decdn_slash_judge_from_block | int > 0 %} +slash_judge_from_block = {{ decdn_slash_judge_from_block }} +{% endif %} [payment] rate_per_mb = {{ decdn_rate_per_mb }} From 495bf93d6c1ad977a34b30c3552e96f964a96900 Mon Sep 17 00:00:00 2001 From: Ant Somers Date: Sat, 11 Jul 2026 16:31:23 +0300 Subject: [PATCH 2/2] fix(decdn_node): render slash_judge_from_block via `| int` for canonical TOML A quoted leading-zero value (e.g. "001") passed the assert and the `| int > 0` gate but rendered the raw string, producing invalid TOML (leading zeros are not TOML integers, so the daemon's config load would fail). Render via `| int` so the emitted value is always canonical. Raised in Copilot PR review. Co-Authored-By: Claude Opus 4.8 (1M context) --- ansible/roles/decdn_node/templates/node.toml.j2 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/roles/decdn_node/templates/node.toml.j2 b/ansible/roles/decdn_node/templates/node.toml.j2 index 8ea898e..a69b5f3 100644 --- a/ansible/roles/decdn_node/templates/node.toml.j2 +++ b/ansible/roles/decdn_node/templates/node.toml.j2 @@ -24,7 +24,7 @@ slash_judge_address = "{{ decdn_slash_judge_address }}" slash_appeal_address = "{{ decdn_slash_appeal_address }}" {% endif %} {% if decdn_slash_judge_from_block | int > 0 %} -slash_judge_from_block = {{ decdn_slash_judge_from_block }} +slash_judge_from_block = {{ decdn_slash_judge_from_block | int }} {% endif %} [payment]