Skip to content

Commit c0243d7

Browse files
committed
docs: Add ADR for V3 node opt-out metadata storage mechanism
1 parent 19e8b0e commit c0243d7

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# 26. V3 Node Opt-Out Metadata for V4 Account Linkage
2+
3+
Date: 2026-02-26
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
ADR-0025 introduced the `NodeV3BillingOptOut` mechanism allowing farmers to signal that their nodes are entering a migration window to Mycelium (v4). Once a node is opted out, the marketplace needs a way to associate that node with a v4 account so that:
12+
13+
1. The v4 marketplace can verify the node is legitimately transitioning and not being double-billed.
14+
2. The v4 marketplace can attribute the node's resources and uptime to the correct farmer identity on v4.
15+
3. The linking is farmer-controlled, on-chain, and auditable — no off-chain oracle or manual mapping is required.
16+
17+
### Options Considered
18+
19+
**Option A: Hero Ledger — mutual authentication via cross-chain signed proof**
20+
The proposal was a **mutual authentication** scheme: a v4 account that wants to claim ownership of a v3 node would sign a proof using the v3 farmer private key (proving control of the v3 account), then submit a transaction from the v4 account to store that signed proof in Hero Ledger. Any verifier could then fetch the proof from KVS, retrieve the v3 farmer's public key from TFChain, and verify the signature — establishing that the v3 and v4 accounts are controlled by the same party without requiring a transaction on TFChain.
21+
22+
Rejected because:
23+
24+
- The chosen approach (Option D) achieves the same ownership guarantee more simply: the farmer signs a TFChain extrinsic from their v3 account, which is already the authoritative proof of ownership.
25+
26+
**Option B: TFChain `kvstore` pallet**
27+
Use the existing generic key-value store pallet already deployed on TFChain. Farmers could write their v4 account under a well-known key (e.g. `node:<node_id>:v4_account`). Rejected because the `kvstore` pallet is scoped per twin — any twin can write to its own namespace but there is no way to enforce that the writer is the farm owner of a specific node, or to enforce that the node must be opted out before the key can be set. The linkage would be self-asserted with no on-chain validation of the ownership relationship, making it unsuitable as a trust anchor for the marketplace verifier.
28+
29+
**Option C: Extend `NodeV3BillingOptOut` map (inline struct)**
30+
Change the existing `NodeV3BillingOptOut` storage value type from a bare `u64` timestamp to a struct containing both the timestamp and optional metadata. Rejected because it requires a storage migration for all existing opted-out nodes, couples two distinct concerns (immutable billing state and mutable v4 linkage) into one storage item, and makes future independent evolution of either field harder.
31+
32+
**Option D: Separate opt-out-gated storage map in pallet-tfgrid (chosen)**
33+
Add a new `NodeV3OptOutMetadata` storage map keyed by `node_id`, only writable when the node has already opted out. This is additive (no migration), co-located with node data, farmer-controlled, and enforces the invariant that metadata is only meaningful for opted-out nodes.
34+
35+
### Per-Node vs Per-Farm Storage
36+
37+
An alternative keying was discussed: storing one metadata entry per farm rather than per node. This would allow a single `set` call to link all nodes on a farm to one v4 account. Rejected in favour of per-node keying because:
38+
39+
- Nodes on the same farm may migrate at different times and could legitimately map to different v4 accounts.
40+
- The opt-out itself (`NodeV3BillingOptOut`) is per-node, so the metadata key should match to keep the relationship unambiguous.
41+
42+
Per-node keying is more granular, consistent with the existing opt-out model, and keeps the linkage lookup O(1) by node ID.
43+
44+
## Decision
45+
46+
### New Storage Item (pallet-tfgrid)
47+
48+
```
49+
NodeV3OptOutMetadata: StorageMap<node_id (u32) → BoundedVec<u8, 256>>
50+
```
51+
52+
- Keyed by node ID; presence is independent of `NodeV3BillingOptOut` at the storage level but enforced at the extrinsic level.
53+
- Max 256 bytes — sufficient to hold any account address format (SS58, hex, bech32) plus a small JSON envelope if needed.
54+
- No storage migration required; purely additive.
55+
56+
### New Extrinsic (pallet-tfgrid)
57+
58+
| Extrinsic | Origin | Call Index |
59+
| --- | --- | --- |
60+
| `set_node_v3_opt_out_metadata(node_id, metadata)` | Farmer (signed) | 46 |
61+
62+
**Preconditions enforced on-chain:**
63+
64+
1. Caller's `AccountId` maps to a twin (`TwinNotExists` otherwise).
65+
2. The node exists (`NodeNotExists` otherwise).
66+
3. The caller's twin is the farm owner twin for the node's farm (`NodeUpdateNotAuthorized` otherwise).
67+
4. The node is already opted out of v3 billing (`NodeNotOptedOutOfV3Billing` otherwise).
68+
5. `metadata.len() ≤ 256` (`NodeV3OptOutMetadataTooLong` otherwise).
69+
70+
**Behaviour:**
71+
72+
- If `metadata` is non-empty: upsert `NodeV3OptOutMetadata[node_id]`, emit `NodeV3OptOutMetadataSet { node_id, metadata }`.
73+
- If `metadata` is empty: remove `NodeV3OptOutMetadata[node_id]`, emit `NodeV3OptOutMetadataCleared { node_id }`.
74+
75+
The extrinsic is idempotent and can be called repeatedly to update or clear the metadata. Only the farm owner can call it, matching the ownership model of `opt_out_of_v3_billing`.
76+
77+
### New Events (pallet-tfgrid)
78+
79+
- `NodeV3OptOutMetadataSet { node_id: u32, metadata: Vec<u8> }` — emitted when metadata is set or updated.
80+
- `NodeV3OptOutMetadataCleared { node_id: u32 }` — emitted when metadata is explicitly cleared.
81+
82+
### New Errors (pallet-tfgrid)
83+
84+
- `NodeNotOptedOutOfV3Billing` — returned when `set_node_v3_opt_out_metadata` is called for a node that has not yet opted out.
85+
- `NodeV3OptOutMetadataTooLong` — returned when the supplied metadata exceeds 256 bytes.
86+
87+
## Flow
88+
89+
### Full opt-out and linkage sequence
90+
91+
```
92+
Farmer
93+
94+
├─1─► opt_out_of_v3_billing(node_id)
95+
│ ├── Guard: caller twin == farm owner twin
96+
│ ├── Guard: node not already opted out
97+
│ ├── Insert: NodeV3BillingOptOut[node_id] = now()
98+
│ └── Emit: NodeV3BillingOptedOut { node_id, opted_out_at }
99+
100+
└─2─► set_node_v3_opt_out_metadata(node_id, v4_account_bytes)
101+
├── Guard: caller twin == farm owner twin
102+
├── Guard: NodeV3BillingOptOut[node_id] exists
103+
├── Guard: len(metadata) ≤ 256
104+
├── Insert: NodeV3OptOutMetadata[node_id] = v4_account_bytes
105+
└── Emit: NodeV3OptOutMetadataSet { node_id, metadata }
106+
```
107+
108+
After step 2, `NodeV3OptOutMetadata[node_id]` holds the farmer's v4 account address (or any agreed-upon linking payload).
109+
110+
### V4 Marketplace Verification Flow
111+
112+
When a node registers or reports uptime on the v4 marketplace, the marketplace verifier must:
113+
114+
```
115+
V4 Marketplace Verifier
116+
117+
├─1─► Query TFChain: NodeV3BillingOptOut[node_id]
118+
│ ├── None → node is NOT in migration window, reject or handle as active v3 node
119+
│ └── Some(opted_out_at) → node is opted out, continue
120+
121+
├─2─► Query TFChain: NodeV3OptOutMetadata[node_id]
122+
│ ├── None → farmer has not yet linked a v4 account, treat as unlinked
123+
│ └── Some(metadata) → decode as v4 account address
124+
125+
├─3─► Verify that the v4 account in metadata matches the account
126+
│ that the node is reporting from on the v4 network
127+
│ ├── Mismatch → reject; farmer must update metadata or re-register
128+
│ └── Match → node is verified as legitimately transitioned
129+
130+
└─4─► Attribute node resources and uptime to the verified v4 account
131+
```
132+
133+
This means the v4 marketplace does **not** trust the node's self-reported identity alone — it cross-checks against the on-chain metadata set by the farm owner, which is the authoritative source.
134+
135+
### Metadata Content Convention
136+
137+
The metadata field is opaque bytes at the pallet level. A UTF-8 JSON object can be used for richer payloads, provided it stays within 256 bytes. For example:
138+
139+
```json
140+
{"schema":"v1","account":"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"}
141+
```
142+
143+
The marketplace should document and enforce its expected format. The pallet enforces only the length bound.
144+
145+
## Consequences
146+
147+
- **No storage migration**: additive change only; existing nodes and storage layouts are unaffected.
148+
- **Farmer-controlled linkage**: the farm owner has sole authority to set or update the v4 account link, matching the existing ownership model.
149+
- **Invariant enforced on-chain**: metadata can only exist for opted-out nodes, preventing invalid or premature linking.
150+
- **Auditable**: all set and clear operations emit events, providing a full on-chain history of linkage changes.
151+
- **Marketplace trust model**: the v4 marketplace must perform two chain queries (opt-out status + metadata) to verify a node. This is a read-only operation and adds no write overhead to the critical paths.
152+
- **Clearing supported**: farmers can clear the metadata by passing empty bytes, which removes the storage entry entirely.

0 commit comments

Comments
 (0)