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
44 changes: 32 additions & 12 deletions ansible/inventory/host_vars/decdn-node-1.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,49 @@
# Per-node deployment values. Copy to inventory/host_vars/<node-name>.yml
# (git-ignored) and fill in. The node refuses to start until these are set.
#
# Protocol facts (chain id, contract addresses) are NOT invented here — source
# them from the canonical deployment / an ADR in decdn/adr/ and cite it.
# The contract addresses below are the canonical Arbitrum Sepolia (chain 421614)
# v0.1.0 genesis deploy, cited to decdn/contracts/deployments/421614.json
# (deployBlock 11249862) — public on-chain facts, not invented here. If you target
# a different deployment, replace them (and chain_id) with that deployment's values.
# MUST-EDIT lines are yours to fill; the rest match the genesis deploy.

# Pinned release to install (a v<version> GitHub Release must exist).
decdn_node_version: "0.1.0"
# Recommended: pin the tarball's sha256 (no checksums file is published upstream).
decdn_node_sha256: ""

# --- Chain (required; sourced from the deployment / ADR — do not guess) -------
decdn_rpc_url: "https://YOUR-ARB-SEPOLIA-RPC/with-key" # SENSITIVE (may embed an API key)
# --- Chain (Arbitrum Sepolia, chain 421614) -----------------------------------
# MUST-EDIT — SENSITIVE (may embed an API key). The public endpoint below works
# for light use; run your own or use a provider for production reliability.
decdn_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc"
decdn_chain_id: 421614 # Arbitrum Sepolia
# Non-hex sentinels so a half-filled copy fails the deploy-time asserts loudly
# (the zero address would otherwise slip through). Replace with real addresses.
decdn_payment_channel_address: "0xREPLACE_PaymentChannel_FROM_DEPLOYMENT"
decdn_capacity_bond_address: "0xREPLACE_CapacityBond_FROM_DEPLOYMENT"
decdn_slash_judge_address: "0xREPLACE_SlashJudge_FROM_DEPLOYMENT"
# Required contracts (deployments/421614.json).
decdn_payment_channel_address: "0xb4bcA0AbF679212708164dCAb98eFa621Fa0F4d3" # PaymentChannel
decdn_capacity_bond_address: "0x2aF490628579c08DC6D0B013090cDC11D4d60Dd4" # CapacityBond
decdn_slash_judge_address: "0x20abcCC80F595a586f4Bd906a1b00c4196416EB7" # SlashJudge

# --- 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
decdn_slash_appeal_address: "0x0D60c0AbffBb5D612B66DB6bDd0d741BB36072cF" # SlashAppeal
decdn_slash_judge_from_block: 11249862 # SlashJudge deploy block — bounds per-restart rescan

# --- Origin directory (optional; ADR 022 — set both or neither) ---------------
decdn_origin_assignment_address: "0x06b394f497481c33FB5ae164d5FbB0307299546c" # OriginAssignment
decdn_publisher_registry_address: "0x32c811eA20326B911B518c2Cf38f9f172aC02e6c" # PublisherRegistry
decdn_origin_directory_from_block: 11249862 # PublisherRegistry deploy block

# --- Content blacklist compliance (optional; ADR 011/031) ---------------------
decdn_content_blacklist_address: "0xd4b2b7CC768c14004dC3400743E2EDB0724355f3" # ContentBlacklist
decdn_content_blacklist_from_block: 11249862 # ContentBlacklist deploy block

# --- Cache pull-through origin (what the node fetches on a cache miss) ---------
# MUST-EDIT — operator-specific backing origin (NOT a chain fact). A serving node
# needs one, else cache misses fail NoOrigin. http shown; see defaults/main.yml for
# fs / s3 fields.
decdn_cache_origin_kind: "http"
decdn_cache_origin_url: "https://your-origin.example/"

# --- Node identity / locale ---------------------------------------------------
decdn_region: "US" # ISO 3166-1 alpha-2 of the node's physical location
decdn_region: "US" # MUST-EDIT — ISO 3166-1 alpha-2 of the node's physical location
# decdn_relay_url: "" # optional iroh relay for NAT traversal

# --- Economics ----------------------------------------------------------------
Expand Down
24 changes: 20 additions & 4 deletions ansible/roles/decdn_node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,26 @@ Per `decdn/adr/019-node-onboarding.md`, a node only serves paid traffic after
`decdn_region` (ISO 3166-1 alpha-2). Contract addresses/chain-id are protocol
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
Optional (omitted from `node.toml` unless set):

- `decdn_slash_appeal_address` — SlashAppeal contract (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.
- `decdn_origin_assignment_address` + `decdn_publisher_registry_address` — the ADR 022
chain-backed origin directory that gates DHT prefetch. **Set both or neither** (either
alone fails the deploy-time assert); `decdn_origin_directory_from_block` bounds its
per-restart log replay.
- `decdn_content_blacklist_address` — the ADR 011/031 compliance watcher (evicts
blacklisted blobs in your region scope); `decdn_content_blacklist_from_block` bounds
its per-restart log replay. Unset ⇒ no watcher (serving a blacklisted hash past its
compliance window is then slashable with no local protection).
- `decdn_cache_origin_kind` (`http`|`fs`|`s3`) + that kind's fields — the pull-through
origin the node fetches on a cache miss. **A serving node needs one:** unset ⇒ no
`[cache.origin]` and cache misses fail `NoOrigin` (the node can only serve blobs it
already holds).

Source contract addresses / chain-id from the deployment
(`contracts/deployments/<chainId>.json`) or an ADR — never guess. See
`roles/decdn_node/defaults/main.yml` for the full knob list and defaults.

## Network
Expand Down
28 changes: 28 additions & 0 deletions ansible/roles/decdn_node/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ decdn_slash_appeal_address: ""
# 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
# Optional (ADR 022). Chain-backed origin directory that gates DHT prefetch. Set
# BOTH or NEITHER — either alone fails the deploy-time assert. Empty => both keys
# omitted and the directory is deny-all (prefetch finds no authorized origins).
decdn_origin_assignment_address: ""
decdn_publisher_registry_address: ""
# Optional. PublisherRegistry deploy block: bounds the origin-directory log replay
# on restart (absent/0 => scans the whole chain — RPC-heavy on an established L2).
# Only emitted when the pair above is set AND this is > 0.
decdn_origin_directory_from_block: 0
# Optional (ADR 011/031). ContentBlacklist watcher — evicts blacklisted blobs in
# your region scope. Empty => no watcher (serving a blacklisted hash past its
# compliance window is then slashable with no local protection).
decdn_content_blacklist_address: ""
# Optional. ContentBlacklist deploy block; bounds the blacklist log replay on
# restart. Only emitted when decdn_content_blacklist_address is set AND this is > 0.
decdn_content_blacklist_from_block: 0
decdn_region: "" # ISO 3166-1 alpha-2
decdn_chain_id: 421614 # Arbitrum Sepolia (matches decdn-node's --chain-id default)

Expand All @@ -38,6 +54,18 @@ decdn_rate_per_mb: 10 # USDC base units (6 decimals)
# --- Cache --------------------------------------------------------------------
decdn_cache_size_mb: 10240 # 10 GB
decdn_max_blob_size_mb: 1024 # 1 GB
# Pull-through origin (#437): what the node fetches on a cache miss. Empty kind =>
# the [cache.origin] table is omitted and misses fail NoOrigin — a serving node
# needs an origin. Pick ONE kind and set that kind's fields; the others are ignored.
decdn_cache_origin_kind: "" # "" (omit) | http | fs | s3
decdn_cache_origin_url: "" # http: base URL (blobs served at {url}/{blake3_hex})
decdn_cache_origin_decompress: "" # http, optional: "auto" (default) | "strict"
decdn_cache_origin_path: "" # fs: filesystem root
decdn_cache_origin_s3_bucket: "" # s3: bucket name
decdn_cache_origin_s3_region: "" # s3: AWS region (used for SigV4 even with a custom endpoint)
decdn_cache_origin_s3_endpoint_url: "" # s3, optional: custom endpoint for R2/B2/MinIO (omit for AWS)
decdn_cache_origin_s3_path_style: false # s3, optional: path-style addressing (MinIO & many self-hosted)
decdn_cache_origin_s3_prefix: "" # s3, optional: key prefix prepended to every object

# --- Observability (loopback only) --------------------------------------------
decdn_log_level: info
Expand Down
84 changes: 84 additions & 0 deletions ansible/roles/decdn_node/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,90 @@
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.

# The ADR 022 origin directory needs BOTH OriginAssignment + PublisherRegistry (it
# gates prefetch on the pair); the template emits them together, so reject a
# one-sided config here. Runs when EITHER is set — an empty counterpart then fails
# the address regex, turning "both or neither" into a loud, specific failure.
- name: Validate the optional origin-directory contract pair (ADR 022)
ansible.builtin.assert:
that:
- decdn_origin_assignment_address is match('^0x[0-9a-fA-F]{40}$')
- decdn_publisher_registry_address is match('^0x[0-9a-fA-F]{40}$')
- decdn_origin_assignment_address != decdn_zero_address
- decdn_publisher_registry_address != decdn_zero_address
fail_msg: >-
The origin directory (ADR 022) needs BOTH decdn_origin_assignment_address and
decdn_publisher_registry_address set to valid contract addresses (0x + 40 hex,
not the zero address) — or BOTH empty to omit it. Source them from the
deployment (contracts/deployments/<chainId>.json) / ADR 022.
vars:
decdn_zero_address: "0x0000000000000000000000000000000000000000"
when: >-
(decdn_origin_assignment_address | length > 0)
or (decdn_publisher_registry_address | length > 0)
Comment thread
thiras marked this conversation as resolved.

# content_blacklist_address is optional (ADR 011/031); a half-filled/zero value
# would silently render a bad node.toml, so validate it fail-loud when present —
# same posture as the SlashAppeal address.
- name: Validate the optional ContentBlacklist address when set
ansible.builtin.assert:
that:
- decdn_content_blacklist_address is match('^0x[0-9a-fA-F]{40}$')
- decdn_content_blacklist_address != decdn_zero_address
fail_msg: >-
decdn_content_blacklist_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 ContentBlacklist address (source it from the deployment / ADR 011).
vars:
decdn_zero_address: "0x0000000000000000000000000000000000000000"
when: decdn_content_blacklist_address | length > 0
Comment thread
thiras marked this conversation as resolved.

# The *_from_block knobs feed `| int > 0` gates in node.toml.j2, and Jinja's int
# filter silently coerces an unparseable value to 0 — dropping the key and
# reverting to a full-chain rescan with no trace. Assert integer shape (like
# slash_judge_from_block) so a typo fails loud. Unconditional: the default 0 passes.
- name: Validate the origin-directory and blacklist scan-floor blocks are integers
ansible.builtin.assert:
that:
- decdn_origin_directory_from_block | string is match('^[0-9]+$')
- decdn_content_blacklist_from_block | string is match('^[0-9]+$')
fail_msg: >-
decdn_origin_directory_from_block and decdn_content_blacklist_from_block must
be non-negative integers (the respective contract deploy blocks). Got
"{{ decdn_origin_directory_from_block }}" / "{{ decdn_content_blacklist_from_block }}".
Leave them 0 to scan from genesis, or set the deploy block to bound rescan cost.

# The cache pull-through origin is a tagged [cache.origin] table: node.toml.j2
# emits only the chosen kind's fields, and the daemon denies unknown fields. Reject
# an unknown kind or a kind missing its required field(s) so a serving node never
# starts with an unusable (or NoOrigin) cache backend.
- name: Validate the cache pull-through origin when set
ansible.builtin.assert:
that:
- decdn_cache_origin_kind in ["http", "fs", "s3"]
- (decdn_cache_origin_kind != "http") or (decdn_cache_origin_url | length > 0)
- (decdn_cache_origin_kind != "fs") or (decdn_cache_origin_path | length > 0)
- >-
(decdn_cache_origin_kind != "s3")
or (decdn_cache_origin_s3_bucket | length > 0
and decdn_cache_origin_s3_region | length > 0)
Comment thread
thiras marked this conversation as resolved.
# Validate each kind's optional value-shape ONLY for the selected kind — a
# leftover value for a non-selected kind is never rendered, so it must not
# fail an unrelated deploy. http: DecompressMode accepts only auto|strict (a
# typo would render TOML the daemon rejects at load — an opaque crash-loop).
- (decdn_cache_origin_kind != "http") or (decdn_cache_origin_decompress in ["", "auto", "strict"])
# s3: path_style feeds a `| bool` gate in node.toml.j2; require a REAL boolean
# so a quoted "false" is rejected here, not silently coerced to path_style = true.
- (decdn_cache_origin_kind != "s3") or (decdn_cache_origin_s3_path_style is boolean)
fail_msg: >-
decdn_cache_origin_kind must be one of http|fs|s3 and carry that kind's
required fields — http: decdn_cache_origin_url; fs: decdn_cache_origin_path;
s3: decdn_cache_origin_s3_bucket + decdn_cache_origin_s3_region.
decdn_cache_origin_decompress, if set, must be auto|strict, and
decdn_cache_origin_s3_path_style must be a boolean (true/false). Leave the
kind empty to omit [cache.origin] (cache misses then fail NoOrigin).
when: decdn_cache_origin_kind | length > 0
Comment thread
thiras marked this conversation as resolved.

# --- User & directories -------------------------------------------------------
- name: Create decdn system group
ansible.builtin.group:
Expand Down Expand Up @@ -177,7 +261,7 @@
# --- Readiness ----------------------------------------------------------------
- name: Wait for the node metrics endpoint
ansible.builtin.uri:
url: "http://127.0.0.1:{{ decdn_metrics_port }}/metrics"

Check warning on line 264 in ansible/roles/decdn_node/tasks/main.yml

View workflow job for this annotation

GitHub Actions / kics

[MEDIUM] Communication Over HTTP

Using HTTP URLs (without encryption) could lead to security vulnerabilities and risks
status_code: 200
register: decdn_metrics_probe
retries: 30
Expand Down
38 changes: 38 additions & 0 deletions ansible/roles/decdn_node/templates/node.toml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ slash_appeal_address = "{{ decdn_slash_appeal_address }}"
{% if decdn_slash_judge_from_block | int > 0 %}
slash_judge_from_block = {{ decdn_slash_judge_from_block | int }}
{% endif %}
{% if decdn_origin_assignment_address | length > 0 %}
origin_assignment_address = "{{ decdn_origin_assignment_address }}"
publisher_registry_address = "{{ decdn_publisher_registry_address }}"
{% if decdn_origin_directory_from_block | int > 0 %}
origin_directory_from_block = {{ decdn_origin_directory_from_block | int }}
{% endif %}
{% endif %}
{% if decdn_content_blacklist_address | length > 0 %}
content_blacklist_address = "{{ decdn_content_blacklist_address }}"
{% if decdn_content_blacklist_from_block | int > 0 %}
content_blacklist_from_block = {{ decdn_content_blacklist_from_block | int }}
{% endif %}
{% endif %}
Comment thread
thiras marked this conversation as resolved.

[payment]
rate_per_mb = {{ decdn_rate_per_mb }}
Expand All @@ -34,6 +47,31 @@ rate_per_mb = {{ decdn_rate_per_mb }}
cache_dir = "{{ decdn_cache_dir }}"
cache_size_mb = {{ decdn_cache_size_mb }}
max_blob_size_mb = {{ decdn_max_blob_size_mb }}
{% if decdn_cache_origin_kind | length > 0 %}

[cache.origin]
kind = "{{ decdn_cache_origin_kind }}"
{% if decdn_cache_origin_kind == "http" %}
url = "{{ decdn_cache_origin_url }}"
{% if decdn_cache_origin_decompress | length > 0 %}
decompress = "{{ decdn_cache_origin_decompress }}"
{% endif %}
{% elif decdn_cache_origin_kind == "fs" %}
path = "{{ decdn_cache_origin_path }}"
{% elif decdn_cache_origin_kind == "s3" %}
bucket = "{{ decdn_cache_origin_s3_bucket }}"
region = "{{ decdn_cache_origin_s3_region }}"
{% if decdn_cache_origin_s3_endpoint_url | length > 0 %}
endpoint_url = "{{ decdn_cache_origin_s3_endpoint_url }}"
{% endif %}
{% if decdn_cache_origin_s3_path_style | bool %}
path_style = true
{% endif %}
{% if decdn_cache_origin_s3_prefix | length > 0 %}
prefix = "{{ decdn_cache_origin_s3_prefix }}"
{% endif %}
{% endif %}
{% endif %}
Comment thread
thiras marked this conversation as resolved.

[observability]
log_level = "{{ decdn_log_level }}"
Expand Down
Loading