From 2b82e99c271d1e2c4768b8bb019cb7fd6cf83e6e Mon Sep 17 00:00:00 2001 From: muchai254 Date: Wed, 15 Apr 2026 11:55:01 +0300 Subject: [PATCH 1/3] docs: add calculations logic doc --- docu/calculation-logic.md | 532 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 532 insertions(+) create mode 100644 docu/calculation-logic.md diff --git a/docu/calculation-logic.md b/docu/calculation-logic.md new file mode 100644 index 0000000..0a2a5e3 --- /dev/null +++ b/docu/calculation-logic.md @@ -0,0 +1,532 @@ +# How Frontend Nodes and Backend Calculation Logic Fit Together + +This guide traces the complete path from a user editing a canvas node to a recalculated result appearing on screen. + +Before reading this guide, browse at least one rawBit lesson to get a concrete picture of how nodes and wires look on the canvas. Setup instructions are in the [Quick start section of the README](../README.md). + +--- + +## The Big Picture + +rawBit separates two concerns across a frontend and a backend: + +| Side | Technology | Responsibility | +| -------- | ------------------------------- | ------------------------------------------------------------------------------------------- | +| Frontend | React, Vite, `@xyflow/react` | Renders nodes and edges on the canvas, tracks which nodes need recalculation and manages UI state | +| Backend | Python, Flask | Evaluates the Bitcoin math for each calculation node and returns results | + +The two sides communicate through a single HTTP endpoint: `POST /bulk_calculate`. The frontend sends a subgraph (a subset of nodes and edges) to this endpoint. The backend processes the nodes in topological order and returns the same nodes with their `result` fields filled in. The frontend merges those results back into the full canvas graph. + +With that picture in mind, the next section explains what a node actually contains and how its internal fields drive the calculation cycle. + +--- + +## What Is a Calculation Node? + +Every box on the canvas is a React Flow `Node` object. Two node types are purely structural and never participate in calculations: + +- `shadcnGroup` groups other nodes visually and performs no computation. +- `shadcnTextInfo` displays a text annotation on the canvas. + +Every other node has `type: "calculation"`. Its `data` field is a `CalculationNodeData` object (defined in `src/types/flow.ts`) and serves as the contract between the frontend and the backend: + +```jsonc +{ + "id": "node_abc123", + "type": "calculation", + "data": { + "functionName": "hash160_hex", + "dirty": true, + "value": "04a1b2...", + "inputs": { "val": "04a1b2..." }, + "inputStructure": { ... }, + "result": "f3e2d1...", + "error": false, + "extendedError": null + } +} +``` + +The following fields are central to how the calculation cycle works: + +| Field | TypeScript type | Written by | Purpose | +| ---------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `functionName` | `string` | Set once in the flow JSON. Not changed at runtime | Names the entry in `CALC_FUNCTIONS` that the backend calls for this node | +| `dirty` | `boolean` | Set to `true` by `useNodeCalculationLogic` when the user edits a field or connects a wire. Cleared to `false` by the backend after processing | Signals that this node's output is stale and must be recalculated | +| `value` | `string` | Typed by the user in the node's input field | Provides a fallback input when no upstream wire is connected; used by `single_val` nodes | +| `inputs` | `object` | Written by the backend after each successful run | Stores the resolved parameters used in the last execution; displayed in the node inspector | +| `inputStructure` | `InputStructure` | Set once in the flow JSON; not changed at runtime | Describes the visible input fields and their indices; the backend reads this to build ordered parameter lists for `multi_val` nodes | +| `result` | `unknown` | Written by the backend after each successful run | The function's return value; downstream nodes read this via the `get_res` closure in `bulk_calculate_logic` | +| `error` | `boolean` | Set to `true` by the backend on function failure or by the frontend on cycle or network error. Removed from `data` entirely (not set to `false`) by the backend on success | Marks whether the last execution failed | +| `extendedError` | `string` | Written on failure by the backend for calculation errors, or by the frontend for cycle detection, network errors, and timeouts. Stripped from the payload before each outgoing request | The human-readable error description shown in the node inspector | + +--- + +## The Lifecycle of a Calculation + +These nine steps trace the full path from a user interaction to a recalculated canvas. Each step leads directly into the next. + +### Step 1: The user changes an input + +When the user types in a node's text field, `useNodeCalculationLogic` in `src/hooks/useCalculation.ts` handles the change event. It updates `data.value` with the new text, sets `data.dirty` to `true` and clears `data.error` so the node does not continue showing a previous failure while the new input is being processed. + +```ts +// src/hooks/useCalculation.ts, useNodeCalculationLogic +data: { ...node.data, value: newValue, dirty: true, error: false } +``` + +This is the only change made in this step. Downstream nodes are not marked dirty here. The system identifies them in the next steps by traversing the graph forward from this node. + +### Step 2: The calculation hook detects the dirty flag and starts a debounce + +`useGlobalCalculationLogic` in `src/hooks/useCalculation.ts` runs after every render. It scans the full node list for any node where `data.dirty` is `true` and `isCalculableNode` (from `src/lib/flow/nonCalculableNodes.ts`) returns `true`. When it finds at least one such node, it calls `onStatusChange("CALC")` to update the status banner and starts a 500 ms debounce timer using `window.setTimeout`. If the user keeps editing before the timer fires, it is cleared and restarted. This batches rapid edits into a single backend request rather than sending one per keystroke. + +The 500 ms debounce delay is the default value of the `debounceMs` parameter in `useGlobalCalculationLogic`. It can be overridden at the call site if needed. + +### Step 3: Compute the affected subgraph + +When the debounce elapses without interruption, the hook calls `getAffectedSubgraph` in `src/lib/graphUtils.ts`. This function computes the minimal set of nodes and edges the backend needs to produce correct results. + +It starts from the set of dirty nodes as seeds. It then runs two breadth-first searches: one backward through the graph using a reverse adjacency map to collect all ancestors (whose `result` values are needed as inputs to the dirty nodes), and one forward using a forward adjacency map to collect all descendants (whose outputs depend on the new result). Both sets are merged into a single affected subgraph. + +There is **one special case**: if a `concat_all` node appears in the affected set, every node that feeds into it is added to the seeds and both searches run again. `concat_all` assembles its full ordered input list on every run, so all feeding branches must be present in the subgraph even if only one branch changed. + +Nodes outside this subgraph are excluded from the request. For large flows this significantly reduces payload size. + +### Step 4: Check for cycles before sending any request + +`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs Kahn's topological-sort algorithm on the subgraph. The algorithm builds an in-degree map and repeatedly removes nodes whose in-degree reaches zero. If the count of processed nodes is smaller than the number of nodes in the subgraph, a cycle exists. Every node in the subgraph is immediately given: + +```ts +data.error = true; +data.extendedError = "Cycle detected in this sub-graph – calculation aborted."; +``` + +The backend is never called in this case. The user sees the error immediately on the canvas without waiting for a network round-trip. + +If no cycle is found, the function returns `false` and execution continues to Step 5. + +### Step 5: Strip UI-only fields and send the request + +`recalculateGraph` in `src/lib/graphUtils.ts` builds the request body. Before serialising, it passes each node through `stripNodeForBackend`, which removes fields that are either large or irrelevant to the backend: `extendedError`, `scriptDebugSteps`, `scriptSteps`, `taprootTree`, `banner`, `tooltip`, `comment`, `showComment`, `searchMark`, and `groupFlash`. Sending these fields would inflate the payload without benefiting the calculation. + +`recalculateGraph` also reads the backend's `maxPayloadBytes` limit (fetched once from `GET /healthz` and cached in `backendLimitsCache`). If the serialised payload exceeds this limit, the request is aborted before sending and all dirty nodes receive a size-limit error. + +The request body sent to `POST /bulk_calculate` is: + +```json +{ + "nodes": [ ... ], + "edges": [ ... ], + "version": 42 +} +``` + +The `version` integer is incremented on every call to `recalculateGraph` via `++versionRef.current`. It is echoed back in the response so the frontend can detect and discard out-of-order replies. A 5-second `AbortController` timeout is applied to the `fetch` call. If the backend does not respond in time, all dirty nodes are marked with a timeout error and `onStatusChange("ERROR")` is called. + +### Step 6: The backend sorts nodes and executes each one + +The `POST /bulk_calculate` route in `backend/routes.py` performs a per-IP sliding-window budget check via `computation_budget.py` before passing the body to `bulk_calculate_logic` in `backend/graph_logic.py`. If the budget is already exhausted, it returns HTTP 429 immediately without running any calculations. + +Inside `bulk_calculate_logic`: + +**1. Edge sanitisation** `_sanitize_edges` drops any edge whose source or target node ID is not present in the payload. The affected nodes receive preflight errors. This is a defensive check and should not trigger during normal operation. + +**2. Topological sort** `topological_sort` applies Kahn's algorithm to determine evaluation order, ensuring every node's input nodes are processed before it runs. Any nodes caught in a cycle are flagged by `_mark_cycle_errors` and skipped during execution. + +**Node-by-node execution.** For each node ID in topological order: + +1. `CALC_FUNCTIONS[node["data"]["functionName"]]` looks up the Python callable. If no matching entry exists, the node is marked with `error: True` and `extendedError: "No such function '...'"` and the loop moves to the next node. +2. `FUNCTION_SPECS[functionName]["paramExtraction"]` selects the builder from `PARAM_BUILDERS`. The available modes are `"none"`, `"single_val"`, `"multi_val"`, and `"val_with_network"` (described in the next section). +3. The selected builder resolves each input, reading upstream `result` values through the `get_res` closure or falling back to manually stored `inputs` text. +4. `validate_inputs` checks required fields and numeric type constraints defined in `FUNCTION_SPECS`. If a constraint is violated, a `ValueError` is raised before the callable is invoked. +5. The Python callable is invoked with the resolved parameters. Its return value is written to `node["data"]["result"]`. `node["data"]["dirty"]` is then set to `False` and the `error` key is removed from `data` via `data.pop("error", None)`. + +The entire loop runs under the wall-clock budget set by `CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`. If the budget is exceeded, `CalculationTimeoutError` is raised, all remaining dirty nodes are marked with an error message, and the partial results are returned immediately. + +### Step 7: The backend returns its response + +`bulk_calculate_logic` returns the updated node map and an error list to the route handler. The handler serialises the result and selects a status code: + +- HTTP 200 with `{ "nodes": [...], "version": 42 }` when no node errors occurred. +- HTTP 400 with `{ "nodes": [...], "version": 42, "errors": [...] }` when at least one node failed. + +The `nodes` array is included in both cases so the frontend can render the correct error state on the canvas rather than leaving nodes in an indeterminate state. + +### Step 8: Merge results into the full graph + +Back on the frontend, `mergePartialResultsIntoFullGraph` in `src/lib/graphUtils.ts` integrates the returned nodes into the complete client-side graph. It iterates over all nodes currently in the frontend graph: + +- If a node is not present in the backend response, it is returned unchanged. +- If a node is present in the response, the function overlays the updated `data` fields onto the existing client-side node, sets `dirty` to `false`, and propagates any matching entry from the `errors` array into `error` and `extendedError`. +- If the node is a `script_verification` node and the response includes `scriptDebugSteps`, that payload is moved into the `scriptStepsCache` (via `setScriptSteps` from `src/lib/share/scriptStepsCache.ts`) and deleted from the node object. This prevents large debug traces from bloating the undo history or being sent back to the backend on the next request. + +After building the merged array, `setNodes` is called. React re-renders the canvas with the new results. + +### Step 9: Record an undo snapshot + +Once the version number in `useGlobalCalculationLogic` confirms the response is not stale (by comparing `version` to `versionRef.current`), the status banner is updated to either `"OK"` or `"ERROR"`. The snapshot scheduler in `src/hooks/useSnapshotScheduler.ts` then pushes a clean entry into `UndoRedoContext`. This entry stores the updated node and edge state along with the calculation status. Pressing Ctrl+Z later restores the canvas to the state captured in this entry. + +--- + +## How the Backend Resolves Inputs + +Step 6 described execution at a high level. This section explains in detail how the backend turns a node's stored state and incoming wires into the exact parameters passed to each Python function. + +`FUNCTION_SPECS` in `backend/calc_functions/function_specs.py` assigns every function a `paramExtraction` mode. This value tells `bulk_calculate_logic` which builder in `PARAM_BUILDERS` to select. + +### `"none"`: no inputs + +Used by `random_256`. The builder `build_none_params` returns an empty dict and the function is invoked with no arguments. + +### `"single_val"`: one input value + +Used by `hash160_hex`, `double_sha256_hex`, `encode_varint`, and most single-step transformation functions. + +`build_single_val_params` resolves the input as follows: + +1. If an incoming edge exists, the upstream node's `result` is used. +2. If no edge exists, `node["data"]["value"]` (the user-typed text) is used as a fallback. +3. If neither is present, a `ValueError` with the message `"Missing required input 'val'"` is raised. + +There is also an unwired-output guard: if the node has outgoing edges but no incoming value and is not an `identity` or `op_code_select` node, the error `"Unwired input: node has outputs but no incoming value"` is raised. This prevents silently propagating an empty value downstream. + +### `"multi_val"`: an ordered list of values + +Used by `concat_all`, `schnorr_sign_bip340`, `script_verification`, and most template-style nodes that accept several named inputs. + +`build_multi_val_params` calls `_multi_common`, which iterates over the visible field indices defined in `node["data"]["inputStructure"]`. For each index, it applies the following precedence: + +1. Sentinel `__FORCE00__` in `node["data"]["inputs"]["vals"]`: overrides the value to `"00"`. +2. Sentinel `__EMPTY__`: forces the value to an empty string. +3. Sentinel `__NULL__`: passes `None` (used by `musig2_nonce_gen` for the optional extra randomness parameter). +4. An incoming edge at that index position (keyed by `targetHandle` such as `"handle-3"`): the upstream node's `result` is used. +5. A manually typed value in `node["data"]["inputs"]["vals"][index]`. + +The resolved values are assembled into an ordered list in field-index order, so the Python function always receives inputs in the order the flow author defined. + +### `"val_with_network"`: one input value plus a network selector + +Used by address-derivation functions such as `hash160_to_p2pkh_address`, `hash160_to_p2wpkh_address`, and `p2tr_address_from_xonly`. + +`build_val_with_network_params` resolves the main value using the same logic as `build_single_val_params`, then appends `selectedNetwork` from `node["data"]["selectedNetwork"]`. The valid values are `"mainnet"`, `"testnet"`, and `"regtest"`. If the field is absent, the builder defaults to `"regtest"`. + +--- + +## A `POST /bulk_calculate` Request and Response Example + +The following example shows the JSON exchanged for a two-node flow: an `identity` node holding a hex value wired into a `sha256_hex` node. The `sha256_hex` node is dirty because the upstream value just changed. + +### Request + +Both nodes are included in the payload even though only `node_hash` is dirty. The identity node is included because the backend needs its `result` to resolve the wired input when building `sha256_hex`'s parameters. + +```json +{ + "nodes": [ + { + "id": "node_src", + "type": "calculation", + "position": { "x": 100, "y": 150 }, + "data": { + "functionName": "identity", + "value": "68656c6c6f", + "inputs": { "val": "68656c6c6f" }, + "result": "68656c6c6f", + "dirty": false, + "error": false + } + }, + { + "id": "node_hash", + "type": "calculation", + "position": { "x": 350, "y": 150 }, + "data": { + "functionName": "sha256_hex", + "inputs": {}, + "dirty": true, + "error": false + } + } + ], + "edges": [ + { + "id": "edge_1", + "source": "node_src", + "target": "node_hash" + } + ], + "version": 3 +} +``` + +The frontend strips these fields from every node before sending: `extendedError`, `scriptDebugSteps`, `scriptSteps`, `taprootTree`, `banner`, `tooltip`, `comment`, `showComment`, `searchMark`, and `groupFlash`. + +### Success response (HTTP 200) + +The backend returns both nodes with updated `result` values and `dirty` cleared. On a successful run the `error` key is removed from the node's data entirely via `data.pop("error", None)`. It is not set to `false`. + +```json +{ + "nodes": [ + { + "id": "node_src", + "type": "calculation", + "data": { + "functionName": "identity", + "value": "68656c6c6f", + "inputs": { "val": "68656c6c6f" }, + "result": "68656c6c6f", + "dirty": false + } + }, + { + "id": "node_hash", + "type": "calculation", + "data": { + "functionName": "sha256_hex", + "inputs": { "val": "68656c6c6f" }, + "result": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + "dirty": false + } + } + ], + "version": 3 +} +``` + +The `result` value above is the SHA-256 hash of the bytes represented by `68656c6c6f` (the ASCII string "hello" in hex), confirmed by running `hashlib.sha256(bytes.fromhex("68656c6c6f")).hexdigest()`. + +### Error response (HTTP 400) + +If `node_hash` had no upstream wire and no `data.value`, the response would be: + +```json +{ + "nodes": [ + { "id": "node_src", "data": { "result": "68656c6c6f", "dirty": false } }, + { + "id": "node_hash", + "data": { + "functionName": "sha256_hex", + "error": true, + "extendedError": "Calculation failed: Missing required input 'val'", + "dirty": false + } + } + ], + "errors": [ + { "nodeId": "node_hash", "error": "Missing required input 'val'" } + ], + "version": 3 +} +``` + +The `nodes` array is still present in error responses. The frontend uses it to highlight the failing node on the canvas. + +--- + +## A Concrete Example: P2PKH Address Derivation + +The following example walks through what happens when a user builds a Pay-to-Public-Key-Hash (P2PKH) address derivation flow and edits the private key node. + +The flow connects five nodes in sequence: + +![P2PKH_Address_Derivation_Nodes](./P2PKH_Address_Derivation_Nodes.JPG) + +When the user types a new private key into the first `identity` node: + +1. `useNodeCalculationLogic` sets `dirty: true` on that node and clears its `error` flag. +2. The 500 ms debounce begins. No network call happens yet. +3. `getAffectedSubgraph` is called. The private key node is dirty. All four downstream nodes are reachable via forward BFS, so all five nodes and their connecting edges enter the subgraph. +4. `checkForCyclesAndMarkErrors` runs on the subgraph and finds no cycles. +5. `stripNodeForBackend` removes UI-only fields from each node. The stripped payload is sent to `POST /bulk_calculate` with `version: N`. +6. The backend runs `topological_sort` and gets the evaluation order: `identity (privkey) -> public_key_from_private_key -> hash160_hex -> hash160_to_p2pkh_address -> identity (address)`. +7. The backend calls each function in turn: + - `identity(val="")` returns the private key unchanged and writes it to `result`. + - `public_key_from_private_key(val="")` derives the SEC-encoded compressed public key and writes it to `result`. + - `hash160_hex(val="")` computes RIPEMD-160(SHA-256(pubkey bytes)) and writes the 20-byte hash to `result`. + - `hash160_to_p2pkh_address(val="", selectedNetwork="testnet")` encodes the hash with a version byte and Base58Check and writes the address string to `result`. + - The final `identity` node passes the address through unchanged. +8. The backend returns all five nodes with updated `result` fields, HTTP 200. +9. `mergePartialResultsIntoFullGraph` overlays the new data onto the full client-side graph. All five nodes now show their fresh values. +10. `UndoRedoContext` records a snapshot so the user can undo back to the previous private key. + +--- + +## Nodes With Multiple Output Handles + +Most nodes expose a single output: the `result` field. A few functions return structured JSON that the backend unpacks into additional named output values stored in `data["outputValues"]`. + +### `taproot_tweak_xonly_pubkey` + +The function returns a JSON object. The backend writes: +- `output_xonly_pubkey` to `data["result"]`, accessible via output handle `output-0`. +- The parity byte (`"c0"` or `"c1"`) to `data["outputValues"]["output-1"]`. +- The tweak value, if present, to `data["outputValues"]["output-2"]`. + +### `taproot_tree_builder` + +The function returns a JSON object describing the full Taproot script tree. The backend writes: +- The Merkle root to `data["result"]`. +- The full tree structure to `data["taprootTree"]`, which the tree inspector panel reads. +- The selected leaf's Merkle path (determined by `data["taprootLeafIndex"]`) to `data["outputValues"]["output-1"]`. + +### `musig2_nonce_gen` + +The function returns a JSON object containing a public nonce and a secret nonce. The backend writes: +- The public nonce (`pubnonce`) to `data["result"]`. +- The secret nonce (`secnonce`) to `data["outputValues"]["output-1"]`. + +On the frontend these extra handles are declared via `outputPorts` in the node definition and wired normally through React Flow. + +--- + +## Script Verification: a Special Case + +The `script_verification` node runs the Bitcoin Script debugger. Its Python function returns a JSON blob containing `isValid` and a `steps` array with one entry per opcode. Each step records the opcode name, the stack state before and after execution, and which script phase was active. + +The backend writes this blob to `data["scriptDebugSteps"]` and sets `data["result"]` to `"true"` or `"false"` based on `isValid`. The frontend then moves the debug steps into a side-cache by calling `setScriptSteps(nodeId, steps)` from `src/lib/share/scriptStepsCache.ts` and deletes `scriptDebugSteps` from the node's data field: + +```ts +// src/lib/graphUtils.ts, mergePartialResultsIntoFullGraph +if (freshSteps !== undefined) { + setScriptSteps(old.id, freshSteps); // write to side-cache + delete merged.data.scriptDebugSteps; // remove from node tree +} +``` + +Storing the debug steps separately prevents them from inflating undo history snapshots or being included in the next `POST /bulk_calculate` payload. + +--- + +## Where Things are in the Codebase + +| Concern | File and symbol | +| ------------------------------------- | --------------------------------------------------------------------------- | +| Marking nodes dirty on user input | `src/hooks/useCalculation.ts`, `useNodeCalculationLogic` | +| Debounce, subgraph selection, request | `src/hooks/useCalculation.ts`, `useGlobalCalculationLogic` | +| Subgraph algorithm | `src/lib/graphUtils.ts`, `getAffectedSubgraph` | +| Frontend cycle detection | `src/lib/graphUtils.ts`, `checkForCyclesAndMarkErrors` | +| HTTP call to backend | `src/lib/graphUtils.ts`, `recalculateGraph` | +| Merging results back | `src/lib/graphUtils.ts`, `mergePartialResultsIntoFullGraph` | +| Flask routes including `POST /bulk_calculate` | `backend/routes.py` | +| Main graph evaluation loop | `backend/graph_logic.py`, `bulk_calculate_logic` | +| Python calculation functions | `backend/calc_functions/calc_func.py` | +| Param extraction specs | `backend/calc_functions/function_specs.py` | +| Node data TypeScript interface | `src/types/flow.ts`, `CalculationNodeData` | +| Non-calculable node list | `src/lib/flow/nonCalculableNodes.ts` | +| Script debug step cache | `src/lib/share/scriptStepsCache.ts` | + +--- + +## Adding a New Calculation Node + +The following changes are the minimum required to make a new function available on the canvas. + +### 1. Write the Python function + +Add the implementation to `backend/calc_functions/calc_func.py`: + +```python +def my_new_function(val: str) -> str: + # perform the calculation + return result_hex +``` + +### 2. Register the spec in `function_specs.py` + +Add an entry to `FUNCTION_SPECS` in `backend/calc_functions/function_specs.py`: + +```python +"my_new_function": { + "paramExtraction": "single_val", + "params": { + "val": {"type": "string", "required": True} + } +}, +``` + +### 3. Register the callable in `graph_logic.py` + +Import the function and add it to `CALC_FUNCTIONS` in `backend/graph_logic.py`: + +```python +from calc_functions.calc_func import my_new_function + +CALC_FUNCTIONS = { + # existing entries ... + "my_new_function": my_new_function, +} +``` + +### 4. Create the node definition on the frontend + +Add an entry to `src/components/sidebar-nodes.ts` (or `src/components/initial-nodes.ts` for flow defaults). Set `functionName: "my_new_function"` in the `data` block and define `inputStructure` to describe which fields the backend reads when building the parameter list. + +### 5. Write a test + +Add a test to `backend/tests/test_calc_func.py` or `backend/tests/test_graph_logic.py` that covers the happy path and at least one error case. Run `python3 run_all_tests.py` to verify the full test suite stays green. + +--- + +## Common Failure Cases + +### Missing required input on a `single_val` node + +A `single_val` node expects exactly one input: either an upstream wire or a `data.value` typed by the user. If neither is present and the node has outgoing wires, `build_single_val_params` in `backend/graph_logic.py` raises `ValueError("Missing required input 'val'")`. The outer exception handler in `bulk_calculate_logic` catches this and writes: + +```python +data["error"] = True +data["extendedError"] = "Calculation failed: Missing required input 'val'" +data["dirty"] = False +``` + +The error entry `{ "nodeId": "...", "error": "Missing required input 'val'" }` is appended to the errors list and the response is HTTP 400. + +A related guard fires when a `single_val` node has outgoing wires but no incoming value and is not an `identity` or `op_code_select` node. In that case the error message is `"Unwired input: node has outputs but no incoming value"`. + +### Type validation failure + +If `FUNCTION_SPECS` declares `"type": "integer"` for a parameter (for example, `uint32_to_little_endian_4_bytes`) and the user provides a non-integer string, `validate_inputs` raises a `ValueError` before the callable is invoked. The node receives `extendedError: "Calculation failed: Param 'val' must be an integer"` and the response is HTTP 400. + +### Unknown `functionName` + +If `node["data"]["functionName"]` does not match any key in `CALC_FUNCTIONS`, the backend marks the node without entering the execution path at all: + +```python +data["error"] = True +data["extendedError"] = "No such function 'my_typo'" +data["dirty"] = False +``` + +This can happen when a flow JSON has been hand-edited or when a function was renamed without updating saved flows. + +### A node error does not block its downstream nodes + +When a node raises an exception during execution, the backend writes `error: True` but does not clear `data["result"]`. Downstream nodes continue to run using whatever `result` value was left from the previous successful run. If there was no previous result, `get_res` returns `None` and the downstream node will likely fail with `"Missing required input"`. If there was a previous result, the downstream node may succeed using stale data while the upstream node shows an error on the canvas. + +### Stale response discarded by the frontend + +Each call to `recalculateGraph` in `src/lib/graphUtils.ts` increments `versionRef.current` and includes the new value in the request body. The backend echoes it back unchanged. When the response arrives, `useGlobalCalculationLogic` compares `json.version` to `versionRef.current`. If a newer request was sent while the first was in-flight, the version numbers will not match and the response is silently discarded. The canvas waits for the latest response without showing an error. + +### Cycle in the graph + +`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs Kahn's algorithm on the subgraph before any network call. If a cycle is detected, every node in the subgraph receives `error: true` and `extendedError: "Cycle detected in this sub-graph – calculation aborted."` and no request is sent. + +The backend also detects cycles via `_mark_cycle_errors` in `backend/graph_logic.py` as a secondary check. In practice the frontend check fires first. + +### Per-request and per-IP execution timeouts + +The backend enforces two independent limits. The first is `CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`, a per-request wall-clock budget. If evaluation exceeds it, `CalculationTimeoutError` is raised and all remaining dirty nodes receive: + +```python +data["error"] = True +data["extendedError"] = "Flow evaluation exceeded the execution budget of 10.0 seconds" +data["dirty"] = False +``` + +The second is a per-IP sliding-window budget tracked in `backend/computation_budget.py`. If a client has consumed too much server time within the configured window, `POST /bulk_calculate` returns HTTP 429 immediately and no evaluation runs. + +--- + From ddbec1441a6be7270a18c70db24ce4b340f441c8 Mon Sep 17 00:00:00 2001 From: muchai254 Date: Wed, 15 Apr 2026 12:07:23 +0300 Subject: [PATCH 2/3] docs: add image to calculation-logic.md --- docu/P2PKH_Address_Derivation_Nodes.JPG | Bin 0 -> 30022 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docu/P2PKH_Address_Derivation_Nodes.JPG diff --git a/docu/P2PKH_Address_Derivation_Nodes.JPG b/docu/P2PKH_Address_Derivation_Nodes.JPG new file mode 100644 index 0000000000000000000000000000000000000000..8a09e6c48f8efa2542c4841f87a2900e7a0a9a5a GIT binary patch literal 30022 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 literal 0 HcmV?d00001 From 398143c536231f1e49c7fe376047b31c9a431a38 Mon Sep 17 00:00:00 2001 From: muchai254 Date: Thu, 23 Apr 2026 16:37:30 +0300 Subject: [PATCH 3/3] docs: shorten doc --- docu/calculation-logic.md | 427 +++++++------------------------------- 1 file changed, 70 insertions(+), 357 deletions(-) diff --git a/docu/calculation-logic.md b/docu/calculation-logic.md index 0a2a5e3..018ae2f 100644 --- a/docu/calculation-logic.md +++ b/docu/calculation-logic.md @@ -1,34 +1,14 @@ # How Frontend Nodes and Backend Calculation Logic Fit Together -This guide traces the complete path from a user editing a canvas node to a recalculated result appearing on screen. +This guide traces the complete path from a user editing a canvas node to a recalculated result appearing on screen. Before reading this guide, browse at least one rawBit lesson to get a concrete picture of how nodes and wires look on the canvas. Setup instructions are in the [Quick start section of the README](../README.md). ---- - -## The Big Picture - -rawBit separates two concerns across a frontend and a backend: - -| Side | Technology | Responsibility | -| -------- | ------------------------------- | ------------------------------------------------------------------------------------------- | -| Frontend | React, Vite, `@xyflow/react` | Renders nodes and edges on the canvas, tracks which nodes need recalculation and manages UI state | -| Backend | Python, Flask | Evaluates the Bitcoin math for each calculation node and returns results | - -The two sides communicate through a single HTTP endpoint: `POST /bulk_calculate`. The frontend sends a subgraph (a subset of nodes and edges) to this endpoint. The backend processes the nodes in topological order and returns the same nodes with their `result` fields filled in. The frontend merges those results back into the full canvas graph. - -With that picture in mind, the next section explains what a node actually contains and how its internal fields drive the calculation cycle. - ---- - ## What Is a Calculation Node? -Every box on the canvas is a React Flow `Node` object. Two node types are purely structural and never participate in calculations: - -- `shadcnGroup` groups other nodes visually and performs no computation. -- `shadcnTextInfo` displays a text annotation on the canvas. +Two node types exist purely for structure and never participate in calculations. These are `shadcnGroup` which groups nodes visually and `shadcnTextInfo` which shows a text annotation. -Every other node has `type: "calculation"`. Its `data` field is a `CalculationNodeData` object (defined in `src/types/flow.ts`) and serves as the contract between the frontend and the backend: +Every other node on the canvas has `type: "calculation"`. Its `data` field is a `CalculationNodeData` object and forms the contract between the frontend and the backend. ```jsonc { @@ -47,180 +27,99 @@ Every other node has `type: "calculation"`. Its `data` field is a `CalculationNo } ``` -The following fields are central to how the calculation cycle works: - -| Field | TypeScript type | Written by | Purpose | -| ---------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| `functionName` | `string` | Set once in the flow JSON. Not changed at runtime | Names the entry in `CALC_FUNCTIONS` that the backend calls for this node | -| `dirty` | `boolean` | Set to `true` by `useNodeCalculationLogic` when the user edits a field or connects a wire. Cleared to `false` by the backend after processing | Signals that this node's output is stale and must be recalculated | -| `value` | `string` | Typed by the user in the node's input field | Provides a fallback input when no upstream wire is connected; used by `single_val` nodes | -| `inputs` | `object` | Written by the backend after each successful run | Stores the resolved parameters used in the last execution; displayed in the node inspector | -| `inputStructure` | `InputStructure` | Set once in the flow JSON; not changed at runtime | Describes the visible input fields and their indices; the backend reads this to build ordered parameter lists for `multi_val` nodes | -| `result` | `unknown` | Written by the backend after each successful run | The function's return value; downstream nodes read this via the `get_res` closure in `bulk_calculate_logic` | -| `error` | `boolean` | Set to `true` by the backend on function failure or by the frontend on cycle or network error. Removed from `data` entirely (not set to `false`) by the backend on success | Marks whether the last execution failed | -| `extendedError` | `string` | Written on failure by the backend for calculation errors, or by the frontend for cycle detection, network errors, and timeouts. Stripped from the payload before each outgoing request | The human-readable error description shown in the node inspector | - ---- - -## The Lifecycle of a Calculation - -These nine steps trace the full path from a user interaction to a recalculated canvas. Each step leads directly into the next. - -### Step 1: The user changes an input - -When the user types in a node's text field, `useNodeCalculationLogic` in `src/hooks/useCalculation.ts` handles the change event. It updates `data.value` with the new text, sets `data.dirty` to `true` and clears `data.error` so the node does not continue showing a previous failure while the new input is being processed. - -```ts -// src/hooks/useCalculation.ts, useNodeCalculationLogic -data: { ...node.data, value: newValue, dirty: true, error: false } -``` - -This is the only change made in this step. Downstream nodes are not marked dirty here. The system identifies them in the next steps by traversing the graph forward from this node. - -### Step 2: The calculation hook detects the dirty flag and starts a debounce - -`useGlobalCalculationLogic` in `src/hooks/useCalculation.ts` runs after every render. It scans the full node list for any node where `data.dirty` is `true` and `isCalculableNode` (from `src/lib/flow/nonCalculableNodes.ts`) returns `true`. When it finds at least one such node, it calls `onStatusChange("CALC")` to update the status banner and starts a 500 ms debounce timer using `window.setTimeout`. If the user keeps editing before the timer fires, it is cleared and restarted. This batches rapid edits into a single backend request rather than sending one per keystroke. - -The 500 ms debounce delay is the default value of the `debounceMs` parameter in `useGlobalCalculationLogic`. It can be overridden at the call site if needed. - -### Step 3: Compute the affected subgraph - -When the debounce elapses without interruption, the hook calls `getAffectedSubgraph` in `src/lib/graphUtils.ts`. This function computes the minimal set of nodes and edges the backend needs to produce correct results. - -It starts from the set of dirty nodes as seeds. It then runs two breadth-first searches: one backward through the graph using a reverse adjacency map to collect all ancestors (whose `result` values are needed as inputs to the dirty nodes), and one forward using a forward adjacency map to collect all descendants (whose outputs depend on the new result). Both sets are merged into a single affected subgraph. - -There is **one special case**: if a `concat_all` node appears in the affected set, every node that feeds into it is added to the seeds and both searches run again. `concat_all` assembles its full ordered input list on every run, so all feeding branches must be present in the subgraph even if only one branch changed. +| Field | Written by | Purpose | +| --- | --- | --- | +| `functionName` | Flow JSON | Names the entry in `CALC_FUNCTIONS` the backend calls | +| `dirty` | Frontend on user edit | Signals that the node's output is stale | +| `value` | User typing in the node's input field | Fallback input when no upstream wire is connected | +| `inputs` | Backend after each successful run | Resolved parameters used in the last execution | +| `inputStructure` | Flow JSON | Describes visible input fields and their indices | +| `result` | Backend after each successful run | The function's return value | +| `error` | Backend on failure | Marks whether the last execution failed | +| `extendedError` | Backend | Human-readable error shown in the node inspector | -Nodes outside this subgraph are excluded from the request. For large flows this significantly reduces payload size. -### Step 4: Check for cycles before sending any request -`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs Kahn's topological-sort algorithm on the subgraph. The algorithm builds an in-degree map and repeatedly removes nodes whose in-degree reaches zero. If the count of processed nodes is smaller than the number of nodes in the subgraph, a cycle exists. Every node in the subgraph is immediately given: +## The Calculation Lifecycle -```ts -data.error = true; -data.extendedError = "Cycle detected in this sub-graph – calculation aborted."; -``` - -The backend is never called in this case. The user sees the error immediately on the canvas without waiting for a network round-trip. - -If no cycle is found, the function returns `false` and execution continues to Step 5. - -### Step 5: Strip UI-only fields and send the request +The following nine steps trace the full path from a user interaction to a recalculated canvas. -`recalculateGraph` in `src/lib/graphUtils.ts` builds the request body. Before serialising, it passes each node through `stripNodeForBackend`, which removes fields that are either large or irrelevant to the backend: `extendedError`, `scriptDebugSteps`, `scriptSteps`, `taprootTree`, `banner`, `tooltip`, `comment`, `showComment`, `searchMark`, and `groupFlash`. Sending these fields would inflate the payload without benefiting the calculation. +**1. The user changes an input.** -`recalculateGraph` also reads the backend's `maxPayloadBytes` limit (fetched once from `GET /healthz` and cached in `backendLimitsCache`). If the serialised payload exceeds this limit, the request is aborted before sending and all dirty nodes receive a size-limit error. +When the user types in a node's field or connects a wire, `useCalcNodeMutations` in `src/hooks/nodes/useCalcNodeMutations.ts` updates `data.inputs.vals`, sets `data.dirty` to `true`, and clears `data.error`. Downstream nodes are not marked dirty here. -The request body sent to `POST /bulk_calculate` is: +**2. The calculation hook detects the dirty flag and starts a debounce.** -```json -{ - "nodes": [ ... ], - "edges": [ ... ], - "version": 42 -} -``` +`useGlobalCalculationLogic` in `src/hooks/useCalculation.ts` scans the node list after every render. When it finds at least one dirty `calculation` node, it calls `onStatusChange("CALC")` to update the status banner and starts a 500 ms debounce timer. Rapid edits reset the timer rather than dispatching one request per keystroke. -The `version` integer is incremented on every call to `recalculateGraph` via `++versionRef.current`. It is echoed back in the response so the frontend can detect and discard out-of-order replies. A 5-second `AbortController` timeout is applied to the `fetch` call. If the backend does not respond in time, all dirty nodes are marked with a timeout error and `onStatusChange("ERROR")` is called. +**3. Compute the affected subgraph.** -### Step 6: The backend sorts nodes and executes each one +When the debounce elapses, `getAffectedSubgraph` in `src/lib/graphUtils.ts` computes the minimal set of nodes and edges the backend needs. Starting from dirty nodes, it runs a backward breadth-first search to collect all ancestors (whose results are needed as inputs) and a forward search to collect all descendants (whose outputs depend on the new result). If a `concat_all` node appears in the affected set, every node feeding into it is added to the seed set and both searches run again. This is because `concat_all` requires its full ordered input list on every run. -The `POST /bulk_calculate` route in `backend/routes.py` performs a per-IP sliding-window budget check via `computation_budget.py` before passing the body to `bulk_calculate_logic` in `backend/graph_logic.py`. If the budget is already exhausted, it returns HTTP 429 immediately without running any calculations. +**4. Check for cycles before sending.** -Inside `bulk_calculate_logic`: +`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs Kahn's topological-sort algorithm on the subgraph. If the algorithm processes fewer nodes than the subgraph contains, a cycle exists. Every node in the subgraph receives `error: true` and `extendedError: "Cycle detected in this sub-graph – calculation aborted."`. The backend is never called. -**1. Edge sanitisation** `_sanitize_edges` drops any edge whose source or target node ID is not present in the payload. The affected nodes receive preflight errors. This is a defensive check and should not trigger during normal operation. +**5. Strip UI-only fields and send the request.** -**2. Topological sort** `topological_sort` applies Kahn's algorithm to determine evaluation order, ensuring every node's input nodes are processed before it runs. Any nodes caught in a cycle are flagged by `_mark_cycle_errors` and skipped during execution. + `recalculateGraph` in `src/lib/graphUtils.ts` passes each node through `stripNodeForBackend`, which removes fields irrelevant to the backend. It then checks the serialised payload against `maxPayloadBytes` from `GET /healthz`. If the payload is too large, all dirty nodes receive a size-limit error and no request is sent. Otherwise the request goes to `POST /bulk_calculate` with the body `{ "nodes": [...], "edges": [...], "version": N }`, where `N` is incremented on every call via `++versionRef.current`. A 5-second `AbortController` timeout is applied to the fetch call. -**Node-by-node execution.** For each node ID in topological order: +**6. The backend sorts nodes and executes each one.** -1. `CALC_FUNCTIONS[node["data"]["functionName"]]` looks up the Python callable. If no matching entry exists, the node is marked with `error: True` and `extendedError: "No such function '...'"` and the loop moves to the next node. -2. `FUNCTION_SPECS[functionName]["paramExtraction"]` selects the builder from `PARAM_BUILDERS`. The available modes are `"none"`, `"single_val"`, `"multi_val"`, and `"val_with_network"` (described in the next section). -3. The selected builder resolves each input, reading upstream `result` values through the `get_res` closure or falling back to manually stored `inputs` text. -4. `validate_inputs` checks required fields and numeric type constraints defined in `FUNCTION_SPECS`. If a constraint is violated, a `ValueError` is raised before the callable is invoked. -5. The Python callable is invoked with the resolved parameters. Its return value is written to `node["data"]["result"]`. `node["data"]["dirty"]` is then set to `False` and the `error` key is removed from `data` via `data.pop("error", None)`. +The `POST /bulk_calculate` route in `backend/routes.py` checks the per-IP sliding-window budget via `computation_budget.py` before calling `bulk_calculate_logic` in `backend/graph_logic.py`. If the budget is exhausted it returns HTTP 429 immediately. Inside `bulk_calculate_logic`, edges referencing unknown node IDs are dropped, then Kahn's algorithm determines evaluation order. For each node in topological order, the backend looks up the Python callable in `CALC_FUNCTIONS`, selects a parameter builder from `PARAM_BUILDERS` based on the `paramExtraction` mode in `FUNCTION_SPECS`, resolves the inputs, validates them with `validate_inputs` and calls the function. -The entire loop runs under the wall-clock budget set by `CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`. If the budget is exceeded, `CalculationTimeoutError` is raised, all remaining dirty nodes are marked with an error message, and the partial results are returned immediately. +On success, `data["result"]` is written and `data.pop("error", None)` removes any prior error. The entire loop runs within the wall-clock budget set by `CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`. -### Step 7: The backend returns its response +**7. The backend returns its response.** -`bulk_calculate_logic` returns the updated node map and an error list to the route handler. The handler serialises the result and selects a status code: +When all nodes succeed, the route returns HTTP 200 with `{ "nodes": [...], "version": N }`. When at least one node fails, it returns HTTP 400 with `{ "nodes": [...], "version": N, "errors": [...] }`. The `nodes` array is present in both cases so the frontend can render the correct state. -- HTTP 200 with `{ "nodes": [...], "version": 42 }` when no node errors occurred. -- HTTP 400 with `{ "nodes": [...], "version": 42, "errors": [...] }` when at least one node failed. +**8. Merge results into the full graph.** -The `nodes` array is included in both cases so the frontend can render the correct error state on the canvas rather than leaving nodes in an indeterminate state. +`mergePartialResultsIntoFullGraph` in `src/lib/graphUtils.ts` iterates over every node in the full client-side graph. Nodes absent from the response are returned unchanged. Nodes present in the response have their `data` fields overlaid with the returned values and `dirty` set to `false`. Any matching entry in the `errors` array is written into `error` and `extendedError`. `script_verification` debug steps are moved into `scriptStepsCache` (via `src/lib/share/scriptStepsCache.ts`) and deleted from the node object to keep them out of undo history and future payloads. -### Step 8: Merge results into the full graph +**9. Version check and undo snapshot.** -Back on the frontend, `mergePartialResultsIntoFullGraph` in `src/lib/graphUtils.ts` integrates the returned nodes into the complete client-side graph. It iterates over all nodes currently in the frontend graph: - -- If a node is not present in the backend response, it is returned unchanged. -- If a node is present in the response, the function overlays the updated `data` fields onto the existing client-side node, sets `dirty` to `false`, and propagates any matching entry from the `errors` array into `error` and `extendedError`. -- If the node is a `script_verification` node and the response includes `scriptDebugSteps`, that payload is moved into the `scriptStepsCache` (via `setScriptSteps` from `src/lib/share/scriptStepsCache.ts`) and deleted from the node object. This prevents large debug traces from bloating the undo history or being sent back to the backend on the next request. - -After building the merged array, `setNodes` is called. React re-renders the canvas with the new results. - -### Step 9: Record an undo snapshot - -Once the version number in `useGlobalCalculationLogic` confirms the response is not stale (by comparing `version` to `versionRef.current`), the status banner is updated to either `"OK"` or `"ERROR"`. The snapshot scheduler in `src/hooks/useSnapshotScheduler.ts` then pushes a clean entry into `UndoRedoContext`. This entry stores the updated node and edge state along with the calculation status. Pressing Ctrl+Z later restores the canvas to the state captured in this entry. - ---- +`useGlobalCalculationLogic` compares the response `version` to `versionRef.current`. If a newer request was sent while the first was in-flight, the version numbers will not match and the response is silently discarded. If the versions match, `onStatusChange` updates the status banner to `"OK"` or `"ERROR"` and `UndoRedoContext` records a snapshot for Ctrl+Z. ## How the Backend Resolves Inputs -Step 6 described execution at a high level. This section explains in detail how the backend turns a node's stored state and incoming wires into the exact parameters passed to each Python function. - -`FUNCTION_SPECS` in `backend/calc_functions/function_specs.py` assigns every function a `paramExtraction` mode. This value tells `bulk_calculate_logic` which builder in `PARAM_BUILDERS` to select. - -### `"none"`: no inputs - -Used by `random_256`. The builder `build_none_params` returns an empty dict and the function is invoked with no arguments. +`FUNCTION_SPECS` in `backend/calc_functions/function_specs.py` assigns every function a `paramExtraction` mode. This selects the builder the backend uses from `PARAM_BUILDERS` in `backend/graph_logic.py`. -### `"single_val"`: one input value +## Example: P2PKH Address Derivation -Used by `hash160_hex`, `double_sha256_hex`, `encode_varint`, and most single-step transformation functions. +A Pay-to-Public-Key-Hash (P2PKH) address derivation flow connects five nodes in sequence: -`build_single_val_params` resolves the input as follows: - -1. If an incoming edge exists, the upstream node's `result` is used. -2. If no edge exists, `node["data"]["value"]` (the user-typed text) is used as a fallback. -3. If neither is present, a `ValueError` with the message `"Missing required input 'val'"` is raised. - -There is also an unwired-output guard: if the node has outgoing edges but no incoming value and is not an `identity` or `op_code_select` node, the error `"Unwired input: node has outputs but no incoming value"` is raised. This prevents silently propagating an empty value downstream. - -### `"multi_val"`: an ordered list of values - -Used by `concat_all`, `schnorr_sign_bip340`, `script_verification`, and most template-style nodes that accept several named inputs. - -`build_multi_val_params` calls `_multi_common`, which iterates over the visible field indices defined in `node["data"]["inputStructure"]`. For each index, it applies the following precedence: - -1. Sentinel `__FORCE00__` in `node["data"]["inputs"]["vals"]`: overrides the value to `"00"`. -2. Sentinel `__EMPTY__`: forces the value to an empty string. -3. Sentinel `__NULL__`: passes `None` (used by `musig2_nonce_gen` for the optional extra randomness parameter). -4. An incoming edge at that index position (keyed by `targetHandle` such as `"handle-3"`): the upstream node's `result` is used. -5. A manually typed value in `node["data"]["inputs"]["vals"][index]`. +![P2PKH_Address_Derivation_Nodes](./P2PKH_Address_Derivation_Nodes.JPG) -The resolved values are assembled into an ordered list in field-index order, so the Python function always receives inputs in the order the flow author defined. +When the user types a new private key into the first `identity` node: -### `"val_with_network"`: one input value plus a network selector +1. `useCalcNodeMutations` sets `dirty: true` on the identity node and clears its `error` flag. +2. The 500 ms debounce starts. No network call happens yet. +3. `getAffectedSubgraph` starts from the dirty identity node, finds no ancestors, and walks forward through all four downstream nodes. All five nodes and their edges enter the subgraph. +4. `checkForCyclesAndMarkErrors` finds no cycles. +5. `stripNodeForBackend` removes UI-only fields. The payload is sent to `POST /bulk_calculate` with `version: N`. +6. The backend runs `topological_sort` and gets the evaluation order: `identity (privkey)`, `public_key_from_private_key`, `hash160_hex`, `hash160_to_p2pkh_address`, `identity (address)`. +7. The backend calls each function in order: + - `identity(val="")` returns the private key unchanged. + - `public_key_from_private_key(val="")` derives the SEC-encoded compressed public key. + - `hash160_hex(val="")` computes RIPEMD-160(SHA-256(pubkey bytes)) and returns the 20-byte hash. + - `hash160_to_p2pkh_address(val="", selectedNetwork="testnet")` encodes the hash with a version byte and Base58Check and returns the address string. + - The final `identity` node passes the address through unchanged. +8. The backend returns all five nodes with updated `result` fields, HTTP 200. +9. `mergePartialResultsIntoFullGraph` overlays the new data onto the full client-side graph. All five nodes show fresh values. +10. `UndoRedoContext` records a snapshot so the user can undo back to the previous private key. -Used by address-derivation functions such as `hash160_to_p2pkh_address`, `hash160_to_p2wpkh_address`, and `p2tr_address_from_xonly`. -`build_val_with_network_params` resolves the main value using the same logic as `build_single_val_params`, then appends `selectedNetwork` from `node["data"]["selectedNetwork"]`. The valid values are `"mainnet"`, `"testnet"`, and `"regtest"`. If the field is absent, the builder defaults to `"regtest"`. ---- +## Example: `POST /bulk_calculate` Request and Response -## A `POST /bulk_calculate` Request and Response Example +The following example shows the JSON exchanged for a two-node flow, an `identity` node holding a hex value wired into a `sha256_hex` node. The `sha256_hex` node is dirty because the upstream value just changed. -The following example shows the JSON exchanged for a two-node flow: an `identity` node holding a hex value wired into a `sha256_hex` node. The `sha256_hex` node is dirty because the upstream value just changed. +Both nodes are included in the payload even though only `node_hash` is dirty. The backend needs the identity node's `result` to resolve the wired input. ### Request -Both nodes are included in the payload even though only `node_hash` is dirty. The identity node is included because the backend needs its `result` to resolve the wired input when building `sha256_hex`'s parameters. - ```json { "nodes": [ @@ -260,12 +159,10 @@ Both nodes are included in the payload even though only `node_hash` is dirty. Th } ``` -The frontend strips these fields from every node before sending: `extendedError`, `scriptDebugSteps`, `scriptSteps`, `taprootTree`, `banner`, `tooltip`, `comment`, `showComment`, `searchMark`, and `groupFlash`. +The `version` integer is echoed back by the backend and used by the frontend to discard out-of-order responses. `stripNodeForBackend` removes `extendedError`, `scriptDebugSteps`, `scriptSteps`, `taprootTree`, `banner`, `tooltip`, `comment`, `showComment`, `searchMark` and `groupFlash` before the request is sent. ### Success response (HTTP 200) -The backend returns both nodes with updated `result` values and `dirty` cleared. On a successful run the `error` key is removed from the node's data entirely via `data.pop("error", None)`. It is not set to `false`. - ```json { "nodes": [ @@ -295,7 +192,7 @@ The backend returns both nodes with updated `result` values and `dirty` cleared. } ``` -The `result` value above is the SHA-256 hash of the bytes represented by `68656c6c6f` (the ASCII string "hello" in hex), confirmed by running `hashlib.sha256(bytes.fromhex("68656c6c6f")).hexdigest()`. +On a successful run the `error` key is removed from `data` entirely. It is not set to `false`. ### Error response (HTTP 400) @@ -322,211 +219,27 @@ If `node_hash` had no upstream wire and no `data.value`, the response would be: } ``` -The `nodes` array is still present in error responses. The frontend uses it to highlight the failing node on the canvas. - ---- - -## A Concrete Example: P2PKH Address Derivation - -The following example walks through what happens when a user builds a Pay-to-Public-Key-Hash (P2PKH) address derivation flow and edits the private key node. +The `nodes` array is present in error responses. The frontend uses it to highlight the failing node on the canvas. -The flow connects five nodes in sequence: - -![P2PKH_Address_Derivation_Nodes](./P2PKH_Address_Derivation_Nodes.JPG) - -When the user types a new private key into the first `identity` node: - -1. `useNodeCalculationLogic` sets `dirty: true` on that node and clears its `error` flag. -2. The 500 ms debounce begins. No network call happens yet. -3. `getAffectedSubgraph` is called. The private key node is dirty. All four downstream nodes are reachable via forward BFS, so all five nodes and their connecting edges enter the subgraph. -4. `checkForCyclesAndMarkErrors` runs on the subgraph and finds no cycles. -5. `stripNodeForBackend` removes UI-only fields from each node. The stripped payload is sent to `POST /bulk_calculate` with `version: N`. -6. The backend runs `topological_sort` and gets the evaluation order: `identity (privkey) -> public_key_from_private_key -> hash160_hex -> hash160_to_p2pkh_address -> identity (address)`. -7. The backend calls each function in turn: - - `identity(val="")` returns the private key unchanged and writes it to `result`. - - `public_key_from_private_key(val="")` derives the SEC-encoded compressed public key and writes it to `result`. - - `hash160_hex(val="")` computes RIPEMD-160(SHA-256(pubkey bytes)) and writes the 20-byte hash to `result`. - - `hash160_to_p2pkh_address(val="", selectedNetwork="testnet")` encodes the hash with a version byte and Base58Check and writes the address string to `result`. - - The final `identity` node passes the address through unchanged. -8. The backend returns all five nodes with updated `result` fields, HTTP 200. -9. `mergePartialResultsIntoFullGraph` overlays the new data onto the full client-side graph. All five nodes now show their fresh values. -10. `UndoRedoContext` records a snapshot so the user can undo back to the previous private key. - ---- - -## Nodes With Multiple Output Handles - -Most nodes expose a single output: the `result` field. A few functions return structured JSON that the backend unpacks into additional named output values stored in `data["outputValues"]`. - -### `taproot_tweak_xonly_pubkey` - -The function returns a JSON object. The backend writes: -- `output_xonly_pubkey` to `data["result"]`, accessible via output handle `output-0`. -- The parity byte (`"c0"` or `"c1"`) to `data["outputValues"]["output-1"]`. -- The tweak value, if present, to `data["outputValues"]["output-2"]`. - -### `taproot_tree_builder` - -The function returns a JSON object describing the full Taproot script tree. The backend writes: -- The Merkle root to `data["result"]`. -- The full tree structure to `data["taprootTree"]`, which the tree inspector panel reads. -- The selected leaf's Merkle path (determined by `data["taprootLeafIndex"]`) to `data["outputValues"]["output-1"]`. - -### `musig2_nonce_gen` - -The function returns a JSON object containing a public nonce and a secret nonce. The backend writes: -- The public nonce (`pubnonce`) to `data["result"]`. -- The secret nonce (`secnonce`) to `data["outputValues"]["output-1"]`. - -On the frontend these extra handles are declared via `outputPorts` in the node definition and wired normally through React Flow. - ---- - -## Script Verification: a Special Case - -The `script_verification` node runs the Bitcoin Script debugger. Its Python function returns a JSON blob containing `isValid` and a `steps` array with one entry per opcode. Each step records the opcode name, the stack state before and after execution, and which script phase was active. - -The backend writes this blob to `data["scriptDebugSteps"]` and sets `data["result"]` to `"true"` or `"false"` based on `isValid`. The frontend then moves the debug steps into a side-cache by calling `setScriptSteps(nodeId, steps)` from `src/lib/share/scriptStepsCache.ts` and deletes `scriptDebugSteps` from the node's data field: - -```ts -// src/lib/graphUtils.ts, mergePartialResultsIntoFullGraph -if (freshSteps !== undefined) { - setScriptSteps(old.id, freshSteps); // write to side-cache - delete merged.data.scriptDebugSteps; // remove from node tree -} -``` - -Storing the debug steps separately prevents them from inflating undo history snapshots or being included in the next `POST /bulk_calculate` payload. - ---- - -## Where Things are in the Codebase - -| Concern | File and symbol | -| ------------------------------------- | --------------------------------------------------------------------------- | -| Marking nodes dirty on user input | `src/hooks/useCalculation.ts`, `useNodeCalculationLogic` | -| Debounce, subgraph selection, request | `src/hooks/useCalculation.ts`, `useGlobalCalculationLogic` | -| Subgraph algorithm | `src/lib/graphUtils.ts`, `getAffectedSubgraph` | -| Frontend cycle detection | `src/lib/graphUtils.ts`, `checkForCyclesAndMarkErrors` | -| HTTP call to backend | `src/lib/graphUtils.ts`, `recalculateGraph` | -| Merging results back | `src/lib/graphUtils.ts`, `mergePartialResultsIntoFullGraph` | -| Flask routes including `POST /bulk_calculate` | `backend/routes.py` | -| Main graph evaluation loop | `backend/graph_logic.py`, `bulk_calculate_logic` | -| Python calculation functions | `backend/calc_functions/calc_func.py` | -| Param extraction specs | `backend/calc_functions/function_specs.py` | -| Node data TypeScript interface | `src/types/flow.ts`, `CalculationNodeData` | -| Non-calculable node list | `src/lib/flow/nonCalculableNodes.ts` | -| Script debug step cache | `src/lib/share/scriptStepsCache.ts` | - ---- - -## Adding a New Calculation Node - -The following changes are the minimum required to make a new function available on the canvas. - -### 1. Write the Python function - -Add the implementation to `backend/calc_functions/calc_func.py`: - -```python -def my_new_function(val: str) -> str: - # perform the calculation - return result_hex -``` - -### 2. Register the spec in `function_specs.py` - -Add an entry to `FUNCTION_SPECS` in `backend/calc_functions/function_specs.py`: - -```python -"my_new_function": { - "paramExtraction": "single_val", - "params": { - "val": {"type": "string", "required": True} - } -}, -``` - -### 3. Register the callable in `graph_logic.py` - -Import the function and add it to `CALC_FUNCTIONS` in `backend/graph_logic.py`: - -```python -from calc_functions.calc_func import my_new_function - -CALC_FUNCTIONS = { - # existing entries ... - "my_new_function": my_new_function, -} -``` - -### 4. Create the node definition on the frontend - -Add an entry to `src/components/sidebar-nodes.ts` (or `src/components/initial-nodes.ts` for flow defaults). Set `functionName: "my_new_function"` in the `data` block and define `inputStructure` to describe which fields the backend reads when building the parameter list. - -### 5. Write a test - -Add a test to `backend/tests/test_calc_func.py` or `backend/tests/test_graph_logic.py` that covers the happy path and at least one error case. Run `python3 run_all_tests.py` to verify the full test suite stays green. - ---- ## Common Failure Cases -### Missing required input on a `single_val` node +**1. Missing required input.** -A `single_val` node expects exactly one input: either an upstream wire or a `data.value` typed by the user. If neither is present and the node has outgoing wires, `build_single_val_params` in `backend/graph_logic.py` raises `ValueError("Missing required input 'val'")`. The outer exception handler in `bulk_calculate_logic` catches this and writes: - -```python -data["error"] = True -data["extendedError"] = "Calculation failed: Missing required input 'val'" -data["dirty"] = False -``` +A `single_val` node with no upstream wire and no `data.value` produces `extendedError: "Calculation failed: Missing required input 'val'"` (HTTP 400). A wired-output node with no incoming value produces `"Unwired input: node has outputs but no incoming value"` instead. -The error entry `{ "nodeId": "...", "error": "Missing required input 'val'" }` is appended to the errors list and the response is HTTP 400. +**2. Node error does not block downstream nodes.** -A related guard fires when a `single_val` node has outgoing wires but no incoming value and is not an `identity` or `op_code_select` node. In that case the error message is `"Unwired input: node has outputs but no incoming value"`. +When a node fails, the backend sets `error: true` but keeps the previous `data["result"]` in place. Downstream nodes run against that stale value. If no prior result exists they will fail with their own missing-input error. -### Type validation failure +**3. Stale response discarded.** -If `FUNCTION_SPECS` declares `"type": "integer"` for a parameter (for example, `uint32_to_little_endian_4_bytes`) and the user provides a non-integer string, `validate_inputs` raises a `ValueError` before the callable is invoked. The node receives `extendedError: "Calculation failed: Param 'val' must be an integer"` and the response is HTTP 400. +The frontend increments `versionRef.current` on every request and the backend echoes it back. If a newer request was sent while the first was in-flight, the version numbers will not match and the earlier response is silently dropped. -### Unknown `functionName` - -If `node["data"]["functionName"]` does not match any key in `CALC_FUNCTIONS`, the backend marks the node without entering the execution path at all: - -```python -data["error"] = True -data["extendedError"] = "No such function 'my_typo'" -data["dirty"] = False -``` - -This can happen when a flow JSON has been hand-edited or when a function was renamed without updating saved flows. - -### A node error does not block its downstream nodes - -When a node raises an exception during execution, the backend writes `error: True` but does not clear `data["result"]`. Downstream nodes continue to run using whatever `result` value was left from the previous successful run. If there was no previous result, `get_res` returns `None` and the downstream node will likely fail with `"Missing required input"`. If there was a previous result, the downstream node may succeed using stale data while the upstream node shows an error on the canvas. - -### Stale response discarded by the frontend - -Each call to `recalculateGraph` in `src/lib/graphUtils.ts` increments `versionRef.current` and includes the new value in the request body. The backend echoes it back unchanged. When the response arrives, `useGlobalCalculationLogic` compares `json.version` to `versionRef.current`. If a newer request was sent while the first was in-flight, the version numbers will not match and the response is silently discarded. The canvas waits for the latest response without showing an error. - -### Cycle in the graph - -`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs Kahn's algorithm on the subgraph before any network call. If a cycle is detected, every node in the subgraph receives `error: true` and `extendedError: "Cycle detected in this sub-graph – calculation aborted."` and no request is sent. - -The backend also detects cycles via `_mark_cycle_errors` in `backend/graph_logic.py` as a secondary check. In practice the frontend check fires first. - -### Per-request and per-IP execution timeouts - -The backend enforces two independent limits. The first is `CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`, a per-request wall-clock budget. If evaluation exceeds it, `CalculationTimeoutError` is raised and all remaining dirty nodes receive: - -```python -data["error"] = True -data["extendedError"] = "Flow evaluation exceeded the execution budget of 10.0 seconds" -data["dirty"] = False -``` +**4. Cycle detected.** -The second is a per-IP sliding-window budget tracked in `backend/computation_budget.py`. If a client has consumed too much server time within the configured window, `POST /bulk_calculate` returns HTTP 429 immediately and no evaluation runs. +`checkForCyclesAndMarkErrors` in `src/lib/graphUtils.ts` runs before any network call. A detected cycle marks every node in the subgraph with `extendedError: "Cycle detected in this sub-graph – calculation aborted."` and sends no request. ---- +**5. Execution timeouts.** +A per-request wall-clock budget (`CALCULATION_TIMEOUT_SECONDS` in `backend/config.py`) marks all remaining dirty nodes with a timeout error and returns partial results. A separate per-IP sliding-window budget (`computation_budget.py`) returns HTTP 429 immediately if the client has consumed too much server time within the window.