diff --git a/.githooks/_/install.sh b/.githooks/_/install.sh deleted file mode 100755 index a90c9cb65..000000000 --- a/.githooks/_/install.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh - -DIR="$(dirname "$0")/.." - -FLAG_FILE="$DIR/_/.setup" - -if [ ! -f "$FLAG_FILE" ]; then - echo "Linking Git Hooks 🐶..." - git config core.hooksPath "$DIR" - touch "$FLAG_FILE" -fi diff --git a/.githooks/commit-msg b/.githooks/commit-msg deleted file mode 100755 index 9745496f9..000000000 --- a/.githooks/commit-msg +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# Regex for Conventional Commits (Plus Git Vernacular for Merges and Reverts) -CONVENTIONAL_COMMITS_REGEX="^((Merge[ a-z-]* branch.*)|(Revert*)|((build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(.*\))?!?: .*))" - -# Get the commit message -COMMIT_MSG=$(cat "$1") - -# Check if the commit message matches the Conventional Commits format -if [[ ! $COMMIT_MSG =~ $CONVENTIONAL_COMMITS_REGEX ]]; then - echo "❌ Error: Commit message does not follow the Conventional Commits format." - echo "" - echo "Expected format: (): " - echo "" - echo "✅ Examples:" - echo " feat(parser): add ability to parse arrays" - echo " fix(login): handle edge case with empty passwords" - echo " docs: update README with installation instructions" - echo "" - echo "Allowed types: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test" - exit 1 -fi - -# If the commit message is valid, allow the commit -exit 0 diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 78f17f3e5..82d968988 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -28,7 +28,7 @@ jobs: uses: google-github-actions/setup-gcloud@v2 - name: Setup build tools (Erlang, Packer and Rebar3) - run: | + run: | curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo apt-key add - sudo apt-add-repository "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" sudo apt-get update @@ -37,7 +37,7 @@ jobs: git clone https://github.com/erlang/rebar3.git && cd rebar3 && ./bootstrap && sudo mv rebar3 /usr/local/bin/ - name: Build and release AO/HyperBEAM with Rebar3 - run: | + run: | rebar3 clean rebar3 get-deps rebar3 compile @@ -51,7 +51,7 @@ jobs: echo "image_name=${TIMESTAMPED_IMAGE_NAME}" >> "$GITHUB_OUTPUT" - name: Build Packer Image - run: | + run: | packer init . packer validate . packer build -var "image_name=${{ steps.set_image_name.outputs.image_name }}" -var "project_id=${{ env.GCP_PROJECT }}" . @@ -75,13 +75,11 @@ jobs: - name: Setup GCloud SDK uses: google-github-actions/setup-gcloud@v2 - - name: Create Confidential VM + - name: Create VM run: | gcloud compute instances create ${{ env.GCP_INSTANCE_NAME }} \ --zone=${{ env.GCP_ZONE }} \ --machine-type=n2d-standard-2 \ - --min-cpu-platform="AMD Milan" \ - --confidential-compute-type=SEV_SNP \ --maintenance-policy=TERMINATE \ --image-family=ubuntu-2404-lts-amd64 \ --image-project=ubuntu-os-cloud \ @@ -119,7 +117,7 @@ jobs: - name: Setup GCloud SDK uses: google-github-actions/setup-gcloud@v2 - - name: Delete Confidential VM + - name: Delete VM run: | gcloud compute instances delete ${{ env.GCP_INSTANCE_NAME }} \ --project=${{ env.GCP_PROJECT }} \ diff --git a/.gitignore b/.gitignore index f230ac910..a9e107950 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +config.* .rebar3 _build _checkouts @@ -42,4 +43,4 @@ mkdocs-site/ mkdocs-site-id.txt mkdocs-site-manifest.csv -!test/admissible-report-wallet.json \ No newline at end of file +!test/config.json diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..d96d17f3f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "native/lib/secp256k1"] + path = native/lib/secp256k1 + url = https://github.com/bitcoin-core/secp256k1.git diff --git a/.vscode/launch.json b/.vscode/launch.json index e3631816e..4d956ca88 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ "internalConsoleOptions": "openOnSessionStart", "module": "hb_debugger", "function": "start_and_break", - "args": "[${input:moduleName}, ${input:functionName}, [${input:funcArgs}]]" + "args": "[${input:moduleName}, ${input:functionName}, [${input:funcArgs}], <<\"${input:debuggerScope}\">>]" }, { "name": "Attach to a 'rebar3 debugger' node.", @@ -65,6 +65,11 @@ "id": "funcArgs", "type": "promptString", "description": "(Optional) Pass arguments to the function:" + }, + { + "id": "debuggerScope", + "type": "promptString", + "description": "(Optional) Additional modules/prefixes for debugger scope:" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f8cbdec3e..e65420ea2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "editor.detectIndentation": false, "editor.insertSpaces": true, - "editor.tabSize": 4 + "editor.tabSize": 4, + "editor.rulers": [80] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index af5da0a2c..4433eda60 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -14,7 +14,37 @@ { "label": "Stop HyperBEAM", "type": "shell", - "command": "lsof -i tcp:10000 | tail -n 1 | awk '{print $2}' | xargs kill -9" + "command": "lsof -i tcp:8734 | tail -n 1 | awk '{print $2}' | xargs kill -9" + }, + { + "label": "Generate a flame graph for a function.", + "type": "shell", + "command": "rebar3 as eflame shell --eval \"hb_debugger:profile_and_stop(fun() -> ${input:moduleName}:${input:functionName}(${input:funcArgs}) end).\"", + "group": "test", + "problemMatcher": "$erlang", + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "new" + } + } + ], + "inputs": [ + { + "id": "moduleName", + "type": "promptString", + "description": "Enter module:" + }, + { + "id": "functionName", + "type": "promptString", + "description": "Enter an exported function name:" + }, + { + "id": "funcArgs", + "type": "promptString", + "description": "(Optional) Pass arguments to the function:" } ] } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..067f7f7ba --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +This repository contains HyperBEAM, an implementation of the AO-Core protocol. + +To familiarize yourself with AO-Core, read the `README.md` file. + +To understand how to write code for HyperBEAM, read `CONTRIBUTING.md` for +repository-level guidelines, and `docs/misc/hacking-on-hyperbeam.md` learn about +its debugging tools and infrastructure. + +In addition to the rules outlined in `CONTRIBUTING.md`, you should abide by the +following: + +1. Always be surgical in your edits. Minimize the line-of-code changes you make + during every single edit. +2. Before adding new utilities, search for existing utilities that do something + similar. Candidates are often found in `hb_ao`, `hb_util`, and `hb_test_utils`. +3. Ensure that you understand the differences between Erlang map terms and + AO-Core's messages. Messages are built using maps under-the-hood, but may also + be lazy-loaded (linkified), giving them different semantics. +4. Before submitting any code as 'complete', you **must** validate that your + new changes do not break any existing tests across the full suite. You are + never being asked to write a 'toy' implementation of features or changed. Your + code must actually work in-production. +5. Always attempt to leave the codebase in a better state than you found it. More + precise, clear, and minimal -- while maintaining the existing featureset. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..720ce0177 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,131 @@ +# Contributing to HyperBEAM. + +There are three basic rules for writing and merging PRs to HyperBEAM: +1. The PR must not introduce additional test failures, flakes, or + removal/defeating of existing tests unless agreed by multiple maintainers. +2. Modifications to the 'kernel layer' must never be made where modifications to + the 'application layer' would suffice. +3. Merged code must abide by the existing style in the repo. Just write and merge + code that blends in. This rule sounds unimportant, but over time it is what makes + the code maintainable and understandable by a larger set of developers. + Spaghetti/mixed styles lowers comprehension, which in a security sensitive + environment => bugs => lost value. No broken windows if we can help it. + +# The HyperBEAM Style Guide. + +**Rule one of style guide club:** _We do not talk about style guide club._ + +We are here to build a fully decentralized alternative to cyberspace as it +is currently constructed. We are not interested in long conversations about +where to put commas or spaces. + +**Rule two of style guide club:** _Blend in._ + +Rule one does not imply that we do not care about the quality of the codebase. +Far from it: We know that we will be maintaining this code for decades to come. +It is important that we are all aligned on style and patterns, but less important +what those styles and patterns actually are. Having `length(Contributors)` +styles adds overhead to understanding the codebase, which over time hides bugs +and reduces maintainability, but each stylistic choice is largely an opinion +that -- despite strong feelings -- lacks criticality. Hence, rule two: +Only write and merge code that actually _blends in_. + +Write your code as if you were the author of all of the existing code. If all +of the other code is written in a certain style, then copy it. If the style +of the code in your PR would not _blend in_, then its style is objectively +in violation of `style guide club`'s rules. + +In the event of disagreement, a simple rule should guide our decisions: What +does the majority of the LoC in the codebase already do? Do that. Then get +back to hacking. + +If you don't like something about the style, simply contribute. If others +disagree strongly, the existing style will be kept. If your contributions are +seen by others as reasonable and inline with the canon, then it will gradually +become adopted as the standard in the codebase. + +**This concludes the rules of style guide club.** + +Remember: Cypherpunks write code! + +# A Rough Guide to the HyperBEAM `canon` + +You should pick up and continue the style of the codebase as you learn how it +works. There is no real substitute for paying attention. There are, however, a +few basic rules that are widely established and represent the core `canon` of the +codebase. As of time of {{`git blame`}}, there is highest consensus around the +following: + +- Always use `-` over `_` in binary key names. + - Why: In general we try to follow the HTTP semantics RFC 9110, so all keys + should be HTTP-Header-Case. This is the style that has been used for Arweave + data protocols since inception, so to avoid confusion we maintain it in + HyperBEAM. + - Nuances: + - One weirdness we inherit from HTTP-land is that headers are actually + case-insensitive, despite the use of capitals in header descriptions, + over-the-wire they are lower-case in HTTP/2+. AO-Core shoots for the + same semantics for consistency. + - In device key resolutions that have multiple words (for example: + `i_like(Base, Req, Opts) -> {ok, <<"Turtles!">>}.`) you may be tempted + to call `~device@1.0/i_like`. Don't. Instead call `/i-like`. + `hb_ao_device` will normalize the keys and match for you. + - `hb_opts` uses all atoms for its message keys. This is a mistake. It + is nice to be able to lookup keys via atoms (normalizing as above) and + we should maintain this, but under the surface the keys should be + normal-form binaries. To avoid issues when this is translated, perform + `Opts` lookups with only atoms, or use binaries of normal-form if you + must. +- Try to keep lines to around 80 characters-ish. This is not a strict rule because + sometimes an 81-85 character line would be very ugly and harder to follow if split. + Use your judgement. + - Why: Our objective is to keep the code readable. Monster lines, and machine-enforced + strict styles, both butcher this. Human/LLM judgement can help here. +- Add a `%%% @doc` moduledoc to each new module you write, and comment every + function you write with a `%% @doc Description` above it. Inline comments are + prepended with a single `%`. + - Why: This helps humans and LLMs grok your code in the future. It also surfaces + useful information in tooltips etc upstream. + - Nuance: I do not know why the Erlang style uses `%%%` for moduledocs, `%%` for + functions, and `%` for inline comments, but it does. This can help with parsability + for some tooling and the effort-cost is minimal, so we use it. +- Avoid 'waterfalls'-style statements, instead keeping every set of statements + nested such that the start and end of the block are indented inline with each + other. + - Why: This uses slightly more lines, but makes deeply nested code much more + readable and comprehensible. + - Examples: +```erlang + BadForm = lists:map( + fun(X) -> + X * lists:sum(lists:fold( + fun(Y, Acc) -> + Y * Acc + end, + [1,2,3] + )) + end + ), + GoodForm = lists:map( + fun(X) -> + X * + lists:sum( + lists:fold( + fun(Y, Acc) -> + Y * Acc + end, + [1,2,3] + ) + ) + end + ) +``` +There are a few areas where there is no consensus on patterns or style yet: +- Expressing docs in the info/[0,1] call of devices. There are a few different + styles in different devices in the codebase -- if you want to add info response + 'inline' docs try to pick one that already exists and see what works/doesn't. + We will need to unify them at some point. +- `maybe ... end` vs nested `case` expressions. `maybe` seems useful and preferable + in at least some cases, but bubbling the right error -- rather than just an error -- + the caller can sometimes be difficult due to the `else` pattern matching. + Experimentation with patterns here would be good. \ No newline at end of file diff --git a/Makefile b/Makefile index 003998809..66f820419 100644 --- a/Makefile +++ b/Makefile @@ -6,9 +6,14 @@ compile: WAMR_VERSION = 2.2.0 WAMR_DIR = _build/wamr -GENESIS_WASM_BRANCH = tillathehun0/cu-experimental +GENESIS_WASM_BRANCH = feat/hb-unit GENESIS_WASM_REPO = https://github.com/permaweb/ao.git -GENESIS_WASM_SERVER_DIR = _build/genesis-wasm-server +GENESIS_WASM_SERVER_DIR = _build/genesis_wasm/genesis-wasm-server + +HYPERBUDDY_UI_REPO = https://github.com/permaweb/hb-explorer +HYPERBUDDY_UI_PACKAGE_JSON = https://raw.githubusercontent.com/permaweb/hb-explorer/main/package.json +HYPERBUDDY_UI_TARGET = src/html/hyperbuddy@1.0/bundle.js +ARWEAVE_GATEWAY = https://arweave.net ifdef HB_DEBUG WAMR_FLAGS = -DWAMR_ENABLE_LOG=1 -DWAMR_BUILD_DUMP_CALL_STACK=1 -DCMAKE_BUILD_TYPE=Debug @@ -50,11 +55,16 @@ $(WAMR_DIR): --single-branch $(WAMR_DIR)/lib/libvmlib.a: $(WAMR_DIR) - sed -i '742a tbl_inst->is_table64 = 1;' ./_build/wamr/core/iwasm/aot/aot_runtime.c; \ + @if ! grep -Fq 'tbl_inst->is_table64 = 1;' ./_build/wamr/core/iwasm/aot/aot_runtime.c; then \ + awk 'NR == 742 { print; print "tbl_inst->is_table64 = 1;"; next } { print }' \ + ./_build/wamr/core/iwasm/aot/aot_runtime.c > ./_build/wamr/core/iwasm/aot/aot_runtime.c.tmp && \ + mv ./_build/wamr/core/iwasm/aot/aot_runtime.c.tmp ./_build/wamr/core/iwasm/aot/aot_runtime.c; \ + fi; \ cmake \ $(WAMR_FLAGS) \ -S $(WAMR_DIR) \ -B $(WAMR_DIR)/lib \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 \ -DWAMR_BUILD_TARGET=$(WAMR_BUILD_TARGET) \ -DWAMR_BUILD_PLATFORM=$(WAMR_BUILD_PLATFORM) \ -DWAMR_BUILD_MEMORY64=1 \ @@ -98,9 +108,26 @@ setup-genesis-wasm: $(GENESIS_WASM_SERVER_DIR) echo "Error: Node.js is not installed. Please install Node.js before continuing."; \ echo "For Ubuntu/Debian, you can install it with:"; \ echo " curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && \\"; \ - echo " apt-get install -y nodejs && \\"; \ + echo " apt-get install -y nodejs=22.16.0-1nodesource1 --allow-downgrades && \\"; \ echo " node -v && npm -v"; \ exit 1; \ fi @cd $(GENESIS_WASM_SERVER_DIR) && npm install > /dev/null 2>&1 && \ echo "Installed genesis-wasm@1.0 server." + +# Update hyperbuddy-ui from remote bundle +update-hyperbuddy-ui: + @echo "Fetching package.json from $(HYPERBUDDY_UI_REPO)..." && \ + TX_ID=$$(curl -s "$(HYPERBUDDY_UI_PACKAGE_JSON)" | grep -o '"bundle"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4) && \ + if [ -z "$$TX_ID" ]; then \ + echo "Error: Could not find 'bundle' field in package.json"; \ + exit 1; \ + fi && \ + echo "Found transaction ID: $$TX_ID" && \ + if [ -f "$(HYPERBUDDY_UI_TARGET)" ]; then \ + rm "$(HYPERBUDDY_UI_TARGET)" && \ + echo "Removed existing bundle.js"; \ + fi && \ + echo "Downloading source code from Arweave..." && \ + curl -sL "$(ARWEAVE_GATEWAY)/$$TX_ID" -o "$(HYPERBUDDY_UI_TARGET)" && \ + echo "Successfully updated $(HYPERBUDDY_UI_TARGET)" diff --git a/README.md b/README.md index fc1b805bd..4893bfb19 100644 --- a/README.md +++ b/README.md @@ -24,20 +24,16 @@ representations regarding the programs that operate inside the AO-Core protocol. models (`devices`) to be executed inside the AO-Core protocol, while enabling their states and inputs to be calculated and committed to in a unified format. -## What is HyperBeam? +## Contributing -HyperBeam is a client implementation of the AO-Core protocol, written in Erlang. -It can be seen as the 'node' software for the decentralized operating system that -AO enables; abstracting hardware provisioning and details from the execution of -individual programs. +HyperBEAM is developed as an open source implementation of the AO-Core protocol +by [Forward Research](https://fwd.arweave.net). Pull Requests are always welcome! -HyperBEAM node operators can offer the services of their machine to others inside -the network by electing to execute any number of different `devices`, charging -users for their computation as necessary. +To get started building on HyperBEAM, check out the +[hacking on HyperBEAM](./docs/misc/hacking-on-hyperbeam.md) and [contribution](./CONTRIBUTING.md) +guides. -Each HyperBEAM node is configured using the `~meta@1.0` device, which provides -an interface for specifying the node's hardware, supported devices, metering and -payments information, amongst other configuration options. +# Building and Running HyperBEAM ## Getting Started @@ -127,17 +123,12 @@ installation is working properly. HyperBEAM can be configured using a `~meta@1.0` device, which is initialized using either command line arguments or a configuration file. -### Configuration with `config.flat` - -The simplest way to configure HyperBEAM is using the `config.flat` file: +### Configuration with `config.json` -1. A file named `config.flat` is already included in the project directory -2. Update to include your configuration values: - -``` -port: 10000 -priv_key_location: /path/to/wallet.json -``` +The simplest way to configure HyperBEAM is using the `config.json` file. It allows +you to configure various aspects of the node's execution environment via +modification of the environmental parameters of the node. Visit +`~meta@1.0/info/format~hyperbuddy@1.0` for a list of available configuration options. 3. Start HyperBEAM with `rebar3 shell` @@ -149,7 +140,7 @@ settings in the startup log. For production environments, you can create a standalone release: ```bash -rebar3 release +HB_CONFIG=path-to-config.json rebar3 release ``` This creates a release in `_build/default/rel/hb` that can be deployed independently. @@ -194,13 +185,13 @@ If a `message` does not explicitly specify a `device`, its implied `device` is a ## Devices HyperBeam supports a number of different devices, each of which enable different -services to be offered by the node. There are presently 25 different devices -included in the `preloaded_devices` of a HyperBEAM node, although it is possible -to add and remove devices as necessary. +services to be offered by the node. HyperBEAM ships with a preloaded store of +packaged devices, although it is possible to add and remove devices as +necessary. ### Preloaded Devices -The following devices are included in the `preloaded_devices` of a HyperBEAM node: +The following devices are included in HyperBEAM's preloaded store: - `~meta@1.0`: The `~meta@1.0` device is used to configure the node's hardware, supported devices, metering and payments information, amongst other configuration options. @@ -290,12 +281,3 @@ python3 -m http.server 8000 ``` For more details on the documentation structure, how to contribute, and other information, please see the [full documentation README](./docs/README.md). - -## Contributing - -HyperBEAM is developed as an open source implementation of the AO-Core protocol -by [Forward Research](https://fwd.arweave.net). Pull Requests are always welcome! - -To get started building on HyperBEAM, check out the [hacking on HyperBEAM](./docs/misc/hacking-on-hyperbeam.md) -guide. - diff --git a/config.flat b/config.flat deleted file mode 100644 index cbcd9ebf3..000000000 --- a/config.flat +++ /dev/null @@ -1 +0,0 @@ -port: 10000 \ No newline at end of file diff --git a/config/app.config b/config/app.config new file mode 100644 index 000000000..6ef0c7446 --- /dev/null +++ b/config/app.config @@ -0,0 +1,7 @@ +[ + {prometheus, [ + {cowboy_instrumenter, [ + {duration_buckets, [0.001, 0.01, 0.1, 0.25, 0.5, 0.75, 1, 2, 4, 10, 30, 60]} + ]} + ]} +]. diff --git a/config/vm.args.src b/config/vm.args.src new file mode 100644 index 000000000..8b0bcf145 --- /dev/null +++ b/config/vm.args.src @@ -0,0 +1 @@ +-sname ${HB_ERL_SNAME:-"hb"} \ No newline at end of file diff --git a/docs/build/device-packaging.md b/docs/build/device-packaging.md new file mode 100644 index 000000000..31f0c7089 --- /dev/null +++ b/docs/build/device-packaging.md @@ -0,0 +1,213 @@ +# Device packaging + +HyperBEAM packages every runtime device — kernel-baked or third-party +— into generated `_hb_device_*` BEAM modules. The Forge writes the +normal multi-module form as a deterministic archive of debug-info BEAM +modules. The packaging tooling lives in +`src/forge`, ships as a rebar3 plugin under one canonical namespace +(`device`), and is the only path for getting a device into a running +node. + +## What the packager does + +For each `dev_` namespace under your source tree (root + +optional `dev__*` helpers): + +1. Read every file in deterministic order and assemble an AO-Core + message of `{filename, body}` pairs. The unsigned ID of that + message is the device's content hash. +2. Decode that ID to raw bytes and encode it as lowercase, unpadded + base32 — appearing in each generated module's atom name. +3. Compile the root and helpers with the Forge rename transform, producing + their generated namespace while rewriting internal calls. +4. Compile each generated module with `debug_info` and pack the BEAMs + under `ebin/` into a deterministic ZIP archive. Files under a + package `priv/` directory are included under `priv/`; in source + directories with multiple roots, `priv/dev_/` is used for + root-specific files. +5. Build two unsigned AO-Core messages — a `Device-Specification` + (markdown derived from the root module's `%%% @doc` block) and an + `Device-Implementation` (the BEAM archive, with `module-name`, + `archive-format`, `implements-device`, `requires-otp-release`, and + optional `requires-system-architecture` keys) — and sign them with the + configured wallet. + +At load time, `priv/` archive entries are materialized under the +node's implementation resource root: +`HB_DEVICE_IMPLEMENTATION_DIR//` (default: +`_build/device-implementations//`). The same root can +be set in node opts with `<<"device-implementation-dir">>`. Device +modules can locate their extracted files with +`hb_device_archive:implementation_dir(?MODULE)` and then use normal Erlang +file/NIF APIs, including `erlang:load_nif/2`. + +The runtime never loads a raw `dev_*` module. Devices reach the +runtime exclusively as the generated `_hb_device_*` form, no matter +whether they came from the in-repo preloaded-store, an Arweave bundle, +or a peer's gateway. + +## Provider commands + +The plugin exposes one namespace, `device`. Every command shares the +same flag set: + +| Flag | Purpose | Default | +|------|---------|---------| +| `--device-src dir[,dir2]` | Source roots to scan | `src/preloaded` in HyperBEAM, `src` elsewhere | +| `--output-dir dir` | Where to write artifacts | command-specific | +| `--key path` | Wallet keyfile used for signing | `hyperbeam-key.json` | +| `--requires-system-architecture` | Include host architecture requirement metadata | off | +| `-d, --devices p[,p2]` | Restrict to specific `dev_*` roots | (all) | +| `--record[=all\|errors]` | For `device test`, write recorder@1.0 test flights | off | + +### `rebar3 device package` + +Scans `--device-src`, packages each device, and writes +`_hb_device__.beam-archive.zip` to `--output-dir` +(default `_build/device-packages`). + +```text +rebar3 device package + └── _build/device-packages/_hb_device_message_1_0_.beam-archive.zip + └── _build/device-packages/_hb_device_meta_1_0_.beam-archive.zip + └── ... +``` + +### `rebar3 device verify` + +Re-loads each generated archive and asserts: + +* the module's atom is in `_hb_device_*` form; +* the archive loads cleanly with normal Erlang module loading; +* its exports are a superset of the root device's expected handlers; +* helper modules from the source set are *not* loadable under their + original names. + +### `rebar3 device preload` + +Packages, signs, and indexes every discovered device into a +LMDB-backed `preloaded-store`. Output: + +* `/` and `/` — signed + spec and implementation messages, stored as TABM via + `hb_cache:write/2`. +* `/` — a signed flat resolver message whose + fields map each human-readable device name to its spec ID. + `name@1.0` is one of those names, so the runtime can read that + first resolver entry directly before the name device itself is + loaded. +* `_build/hb_preloaded_index.hrl` — a generated compile-time macro + containing the index ID. The build hook recompiles `hb_opts` after + writing it, so the default node config embeds the correct index + without reading a separate metadata file at runtime. + +### `rebar3 device test` + +Builds a fresh preloaded-store from HyperBEAM's built-in preloaded +devices plus `--device-src`, then runs the selected device root EUnit +suites against that store. The store contains the full local source +set so root tests can resolve device dependencies. In an external +device repo this is normally just: + +```bash +rebar3 device test +``` + +Use `rebar3 device test --with-core` to include the normal core +`rebar3 eunit` modules in the same EUnit run. The `rebar3 eunit-all` +alias is shorthand for that full local check. + +Use `--record` or `--record=errors` to write `~recorder@1.0` test flights +for failures, or `--record=all` to write one HTML archive for every test. + +### `rebar3 device local` + +Builds a fresh preloaded-store, points `HB_PRELOADED_STORE` and +`HB_PRELOADED_DEVICES_INDEX` at it, then starts the normal Rebar shell. +Use this when you want a local node that can resolve your packaged +devices immediately: + +```bash +rebar3 device local +``` + +The generated device template configures Rebar shell to start `hb`. +Custom runtime config works through the usual environment: + +```bash +HB_CONFIG=custom.json rebar3 device local +``` + +### `rebar3 device publish` + +Packages, signs, and uploads spec + implementation messages to +Arweave via `dev_arweave`. Before signing, the provider builds the +same local preloaded-store used by `device test`, so the signing path +can resolve HyperBEAM's built-in devices without extra environment +variables. Returns each device's spec and impl IDs on stdout. + +## Configuration the runtime cares about + +| Key | Type | Role | +|-----|------|------| +| `<<"preloaded-store">>` | store map | LMDB preloaded device store. | +| `<<"preloaded-devices-index">>` | binary | Committed ID of the flat preloaded resolver message. Embedded into `hb_opts` from `_build/hb_preloaded_index.hrl` during compilation. | +| `<<"loaded-device-store">>` | store map | Optional shared cache of name/spec-ID → loaded module atom. | +| `<<"trusted-device-signers">>` | `[Address]` | Acceptable signer addresses for impl messages. Defaults to the node wallet. | +| `<<"trusted-devices">>` | `#{NameOrSpecID => ImplID}` | Operator-pinned implementation IDs trusted directly for the named device or spec ID. | +| `<<"load-remote-devices">>` | bool | Whether unmatched devices may be fetched via the Arweave gateway. | +| `<<"admissible-devices">>` | `all` or `[Name]` | Per-execution allowlist (used by the Lua sandbox). | + +`HB_PRELOADED_STORE` and `HB_PRELOADED_DEVICES_INDEX` override the +first two fields for provider-driven test runs, so the nested EUnit +node uses the freshly generated preloaded-store. + +Operators control the bake via the source set their build runs +`rebar3 device preload` over. + +### Forge preload bootstrap + +The preloaded-store builder has one forge-private bootstrap step. To +compute source IDs and sign normal AO-Core messages, the builder +compiles and loads only the minimal build devices under their source +module names: `message@1.0`, `structured@1.0`, and the configured +commitment device (`httpsig@1.0` by default). Those modules are +reachable only through the build-local `forge-bootstrap` option. The +runtime never sets that option, and the seed modules are never written +to the final preloaded-store. + +All final package identities are normal AO-Core unsigned message IDs +of the source-file message, and every final runtime implementation is +loaded from a signed archive message in generated `_hb_device_*` form. + +## Project template + +The Forge also ships a `rebar3 new` template for external device +authors. Install it into the user-level rebar3 template directory from +a HyperBEAM checkout: + +```bash +./install-template --branch edge +``` + +Development checkouts can be used directly: + +```bash +./install-template --local /path/to/hyperbeam +``` + +For reproducible scaffolding, use `--commit COMMIT_SHA`; for a +non-default remote, pair `--branch` or `--commit` with `--repo URL`. +If no source option is given, the installer uses the `edge` branch of +the default HyperBEAM repository. + +Then scaffold a device project: + +```bash +rebar3 new device name=my_device +``` + +The template writes `rebar.config`, `src/.app.src`, +`src/dev_.erl`, `README.md`, and `.gitignore`. Its +`rebar.config` keeps the `hb` dependency and Forge plugin on +the same HyperBEAM ref. diff --git a/docs/build/extending-hyperbeam.md b/docs/build/extending-hyperbeam.md index c033a8d93..7450cf2d7 100644 --- a/docs/build/extending-hyperbeam.md +++ b/docs/build/extending-hyperbeam.md @@ -1,83 +1,94 @@ # Extending HyperBEAM -HyperBEAM's modular design, built on AO-Core principles and Erlang/OTP, makes it highly extensible. You can add new functionalities or modify existing behaviors primarily by creating new **Devices** or implementing **Pre/Post-Processors**. - -!!! warning "Advanced Topic" - Extending HyperBEAM requires a good understanding of Erlang/OTP, the AO-Core protocol, and HyperBEAM's internal architecture. This guide provides a high-level overview; detailed implementation requires deeper exploration of the source code. - -## Approach 1: Creating New Devices - -This is the most common way to add significant new capabilities. -A Device is essentially an Erlang module (typically named `dev_*.erl`) that processes AO-Core messages. - -**Steps:** - -1. **Define Purpose:** Clearly define what your device will do. What kind of messages will it process? What state will it manage (if any)? What functions (keys) will it expose? -2. **Create Module:** Create a new Erlang module (e.g., `src/dev_my_new_device.erl`). -3. **Implement `info/0..2` (Optional but Recommended):** Define an `info` function to signal capabilities and requirements to HyperBEAM (e.g., exported keys, variant/version ID). - ```erlang - info() -> - #{ - variant => <<"MyNewDevice/1.0">>, - exports => [<<"do_something">>, <<"get_status">>] - }. - ``` -4. **Implement Key Functions:** Create Erlang functions corresponding to the keys your device exposes. These functions typically take `StateMessage`, `InputMessage`, and `Environment` as arguments and return `{ok, NewMessage}` or `{error, Reason}`. - ```erlang - do_something(StateMsg, InputMsg, Env) -> - % ... perform action based on InputMsg ... - NewState = ..., % Calculate new state - {ok, NewState}. - - get_status(StateMsg, _InputMsg, _Env) -> - % ... read status from StateMsg ... - StatusData = ..., - {ok, StatusData}. - ``` -5. **Handle State (If Applicable):** Devices can be stateless or stateful. Stateful devices manage their state within the `StateMessage` passed between function calls. -6. **Register Device:** Ensure HyperBEAM knows about your device. This might involve adding it to build configurations or potentially a dynamic registration mechanism if available. -7. **Testing:** Write EUnit tests for your device's functions. - -**Example Idea:** A device that bridges to another blockchain network, allowing AO processes to read data or trigger transactions on that chain. - -## Approach 2: Building Pre/Post-Processors - -Pre/post-processors allow you to intercept incoming requests *before* they reach the target device/process (`preprocess`) or modify the response *after* execution (`postprocess`). These are often implemented using the `dev_stack` device or specific hooks within the request handling pipeline. - -**Use Cases:** - -* **Authentication/Authorization:** Checking signatures or permissions before allowing execution. -* **Request Modification:** Rewriting requests, adding metadata, or routing based on specific criteria. -* **Response Formatting:** Changing the structure or content type of the response. -* **Metering/Logging:** Recording request details or charging for usage before or after execution. - -**Implementation:** - -Processors often involve checking specific conditions (like request path or headers) and then either: - -a. Passing the request through unchanged. -b. Modifying the request/response message structure. -c. Returning an error or redirect. - - -**Example Idea:** A preprocessor that automatically adds a timestamp tag to all incoming messages for a specific process. - - -## Approach 3: Custom Routing Strategies - -While `dev_router` provides basic strategies (round-robin, etc.), you could potentially implement a custom load balancing or routing strategy module that `dev_router` could be configured to use. This would involve understanding the interfaces expected by `dev_router`. - -**Example Idea:** A routing strategy that queries worker nodes for their specific capabilities before forwarding a request. - -## Getting Started - -1. **Familiarize Yourself:** Deeply understand Erlang/OTP and the HyperBEAM codebase (`src/` directory), especially [`hb_ao.erl`](../resources/source-code/hb_ao.md), [`hb_message.erl`](../resources/source-code/hb_message.md), and existing `dev_*.erl` modules relevant to your idea. -2. **Study Examples:** Look at simple devices like `dev_patch.erl` or more complex ones like `dev_process.erl` to understand patterns. -3. **Start Small:** Implement a minimal version of your idea first. -4. **Test Rigorously:** Use `rebar3 eunit` extensively. -5. **Engage Community:** Ask questions in developer channels if you get stuck. - -Extending HyperBEAM allows you to tailor the AO network's capabilities to specific needs, contributing to its rich and evolving ecosystem. +There is one production path for putting a new device into a +HyperBEAM node: write Erlang sources, package them with the Forge, and +let the runtime load the resulting `_hb_device_*` BEAM archive. That +single path covers both the devices baked into the HyperBEAM +repository and third-party devices that ship in their own repos. + +This page is a quick orientation. The reference details live in the +[Device packaging](device-packaging.md) and +[External device repository](external-device-repository.md) guides. + +## The shape of a device + +A device is a namespace of Erlang modules: + +* one root: `dev_.erl` whose exports become the device's + public API; +* optionally one or more helpers: `dev__*.erl` whose functions + are loaded only under the generated `_hb_device_*__*` helper names. + +The root may declare `-implements(<<"name@version">>).` (or a 43-char +specification ID); without it the Forge derives the human name from the +module — `dev_my_thing` → `my-thing@1.0`. + +```erlang +%%% @doc One-paragraph description that becomes the device's +%%% Device-Specification body. Markdown is fine. +-module(dev_my_thing). +-export([info/1, do/3]). + +info(_Opts) -> + #{ exports => [<<"do">>] }. + +do(Base, Req, Opts) -> + %% Implement the device's behaviour by returning {ok, Result} + %% or {error, Reason}. + {ok, Base#{ <<"echo">> => maps:get(<<"input">>, Req, undefined) }}. +``` + +A top-level `%%% @doc` block becomes the spec body; alternatively +`-specification("path/to/spec.md").` points at an out-of-line file. + +## In-repo devices + +The HyperBEAM repository keeps every device source under +`src/preloaded`. The `compile` step runs the same Forge preload pipeline +over that directory and emits an LMDB `preloaded-store` plus the index +ID the core default config consumes: + +```bash +rebar3 compile # builds core + forge, then preloads +rebar3 eunit # runs core tests against the bake +``` + +`hb_device_load:reference/2` resolves every device — including +`message@1.0`, `httpsig@1.0`, etc. — through that store. There is no +privileged kernel path that would let `dev_message` be used directly +as a runtime device. + +## External devices + +Third-party devices live in their own rebar3 projects, depending on +HyperBEAM. Once the Forge template is installed in the user rebar3 +config, create one with: + +```bash +rebar3 new device name=my_device +``` + +Then iterate with: + +```bash +rebar3 device package # build _hb_device_*.beam-archive.zip +rebar3 device verify # check archive invariants +rebar3 device test # run dev_ EUnit against a fresh store +rebar3 eunit-all # run core EUnit plus packaged-device EUnit +rebar3 device publish # sign and upload to Arweave +``` + +See [External device repository](external-device-repository.md) for +the full template. + +## Runtime shape + +The build-time preloaded resolver message maps each name to a signed +specification ID, and the optional `loaded-device-store` runtime cache +maps resolved names and IDs to loaded generated module atoms. Operators +who want to change the baked-in set rebuild the in-repo source set or +point `<<"preloaded-store">>` at a store produced by an external build. + +Codec devices use the same source and runtime naming as every other +device: source modules are `dev_.erl`, and runtime atoms are +`_hb_device__`. diff --git a/docs/build/external-device-repository.md b/docs/build/external-device-repository.md new file mode 100644 index 000000000..b87267d25 --- /dev/null +++ b/docs/build/external-device-repository.md @@ -0,0 +1,158 @@ +# Building a third-party device repository + +Devices live in their own repos as ordinary Erlang projects. The +HyperBEAM Forge ships as a rebar3 plugin under the `device` namespace, +so the workflow looks like a normal rebar3 project plus a few extra +commands. + +## Repository layout + +After installing the Forge template, a new project can be created with: + +```bash +rebar3 new device name=my_device +``` + +The generated project has this shape: + +``` +my-device/ +├── .gitignore +├── README.md +├── rebar.config +└── src/ + ├── my_device.app.src + ├── dev_my_device.erl %% root + └── dev_my_device_helpers.erl %% optional helpers +``` + +The packager treats `dev_.erl` as the root, with any +`dev__*.erl` siblings renamed into the same generated namespace +as helpers. The root module's exports remain the public device API; +helpers are loaded only by their generated names. + +## Installing the template + +From a HyperBEAM checkout, install the Forge template into your user +rebar3 template directory: + +```bash +./install-template --branch edge +``` + +Use a local checkout while developing HyperBEAM itself: + +```bash +./install-template --local /path/to/hyperbeam +``` + +`--local` pins the checkout's committed `HEAD`. Commit or switch to the +desired local revision, then rerun the installer when you want the +template to move. + +Pin to an exact commit for reproducible local scaffolding: + +```bash +./install-template --commit COMMIT_SHA +``` + +The script writes only template files under +`~/.config/rebar3/templates` by default. Pass `--template-dir PATH` to +install them elsewhere. The generated project receives the selected +HyperBEAM dependency and plugin terms in its own `rebar.config`. + +## `rebar.config` + +The generated `rebar.config` pins the `hb` dependency and the Forge +plugin to the same HyperBEAM ref. Rebar requires the +dependency and plugin to be declared separately; keeping their refs +identical ensures the provider and kernel APIs match. + +```erlang +{deps, [ + %% Pull HyperBEAM in so the core modules and Forge are on the + %% code path. Pin to a specific branch, tag, or commit for + %% reproducible builds. + {hb, {git, "https://github.com/permaweb/hyperbeam.git", + {branch, "edge"}}} +]}. + +{plugins, [ + {plugin, + {git_subdir, "https://github.com/permaweb/hyperbeam.git", + {branch, "edge"}, + "src/forge"}} +]}. +``` + +`rebar3` will fetch HyperBEAM, place its core modules on the path +(`hb_ao`, `hb_message`, `hb_cache`, …), and load the Forge plugin. The +Forge uses that `hb` dependency as the source of the +default preloaded device library, then adds your project devices to +the same generated preloaded-store. + +If your device uses HyperBEAM macros such as `?event`, include the +core header from your device module and list `hb` in your app: + +```erlang +-include_lib("hb/include/hb.hrl"). +``` + +## Day-to-day commands + +### Iterate on a device + +```bash +rebar3 device package +rebar3 device verify +``` + +`package` writes the generated BEAM archive to `_build/device-packages/`; +`verify` re-loads it and checks the archive invariants. + +### Run your tests against a fresh preloaded-store + +```bash +rebar3 device test +``` + +`device test` packages HyperBEAM's built-in preloaded devices and the +configured project source set into one temporary `preloaded-store`, +then runs the project device root EUnit suites against it. The store +contains the full local source set so those tests can resolve +dependencies. + +### Start a local node with your device + +```bash +rebar3 device local +``` + +`device local` builds the same kind of preloaded-store, sets the +preloaded-store environment for the shell, and then starts the normal +HyperBEAM node locally. The generated template configures Rebar shell +to start `hb`, so custom app configuration can use the normal +environment form: + +```bash +HB_CONFIG=custom.json rebar3 device local +``` + +### Publish to Arweave + +```bash +rebar3 device publish --key wallet.json +``` + +Each device prints its `spec_id` and `impl_id` on stdout. Operators +who trust your wallet can resolve `dev_my_device` either by name (if +you also publish a `name@1.0` provider message that maps the human +name to the spec ID) or by quoting the spec ID directly. + +## Iterating on HyperBEAM core changes + +Because `hb` is a regular dependency, your editor and `rebar3 shell` +can step into core sources via `_build/default/lib/hb/src/core`. +When you need a core patch your device depends on, ship it as a +separate PR against HyperBEAM and bump the `tag` in your +`rebar.config` to pick it up. diff --git a/docs/build/serverless-decentralized-compute.md b/docs/build/serverless-decentralized-compute.md index cb1c589c6..bb970493c 100644 --- a/docs/build/serverless-decentralized-compute.md +++ b/docs/build/serverless-decentralized-compute.md @@ -25,7 +25,7 @@ WebAssembly (WASM) allows you to run precompiled code written in languages like [aos]> Send({ Target = "AOS", Action = "Spawn", Module = "", Scheduler = "" }) -- This returns a ``` - *(Note: The exact spawning mechanism might vary; consult `aos` or relevant SDK documentation. You specify that this process uses WASM.)* + *(Note: The exact spawning mechanism might vary; consult `aos` or relevant Forge documentation. You specify that this process uses WASM.)* 4. **Send Message to Trigger Execution:** ```lua [aos]> Send({ Target = "", Action = "ExecuteFunction", InputData = "Some data for WASM" }) @@ -74,7 +74,7 @@ If a HyperBEAM node performing these computations runs within a supported Truste This usually involves interacting with nodes specifically advertised as TEE-enabled. The exact mechanism for requesting and verifying attestations depends on the specific TEE technology and node configuration. -* The HTTP response headers might contain specific signature or attestation data (e.g., using HTTP Message Signatures RFC-9421 via [`dev_codec_httpsig`](../resources/source-code/dev_codec_httpsig.md)). +* The HTTP response headers might contain specific signature or attestation data (e.g., using HTTP Message Signatures RFC-9421 via [`dev_httpsig`](../resources/source-code/dev_httpsig.md)). * You might query the [`~snp@1.0`](../resources/source-code/dev_snp.md) device directly on the node to get its attestation report. Refer to documentation on [TEE Nodes](./run/tee-nodes.md) and the [`~snp@1.0`](../resources/source-code/dev_snp.md) device for details. diff --git a/docs/devices/json-at-1-0.md b/docs/devices/json-at-1-0.md index eec5e7985..586dcdad4 100644 --- a/docs/devices/json-at-1-0.md +++ b/docs/devices/json-at-1-0.md @@ -39,4 +39,4 @@ This retrieves the node configuration from the meta device and serializes it to - [Message Device](../resources/source-code/dev_message.md) - Works well with JSON serialization - [Meta Device](../resources/source-code/dev_meta.md) - Can provide configuration data to serialize -[json module](../resources/source-code/dev_codec_json.md) \ No newline at end of file +[json module](../resources/source-code/dev_json.md) \ No newline at end of file diff --git a/docs/devices/message-at-1-0.md b/docs/devices/message-at-1-0.md index 000bdb860..b827819b6 100644 --- a/docs/devices/message-at-1-0.md +++ b/docs/devices/message-at-1-0.md @@ -9,7 +9,7 @@ This device is particularly useful for: * Creating and modifying transient messages on the fly using query parameters. * Retrieving specific values from a message map. * Inspecting the keys of a message. -* Handling message commitments and verification (though often delegated to specialized commitment devices like [`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). +* Handling message commitments and verification (though often delegated to specialized commitment devices like [`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). ## Core Functionality @@ -43,8 +43,8 @@ The `message@1.0` device reserves several keys for specific operations: * **`set_path`**: A special case for setting the `path` key itself, which cannot be done via the standard `set` operation. * **`remove`**: Removes one or more specified keys from the message. Requires an `item` or `items` parameter. * **`keys`**: Returns a list of all public (non-private) keys present in the message map. -* **`id`**: Calculates and returns the ID (hash) of the message. Considers active commitments based on specified `committers`. May delegate ID calculation to a device specified by the message\'s `id-device` key or the default ([`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). -* **`commit`**: Creates a commitment (e.g., a signature) for the message. Requires parameters like `commitment-device` and potentially committer information. Delegates the actual commitment generation to the specified device (default [`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). +* **`id`**: Calculates and returns the ID (hash) of the message. Considers active commitments based on specified `committers`. May delegate ID calculation to a device specified by the message\'s `id-device` key or the default ([`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). +* **`commit`**: Creates a commitment (e.g., a signature) for the message. Requires parameters like `commitment-device` and potentially committer information. Delegates the actual commitment generation to the specified device (default [`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). * **`committers`**: Returns a list of committers associated with the commitments in the message. Can be filtered by request parameters. * **`commitments`**: Used internally and in requests to filter or specify which commitments to operate on (e.g., for `id` or `verify`). * **`verify`**: Verifies the commitments attached to the message. Can be filtered by `committers` or specific `commitment` IDs in the request. Delegates verification to the device specified in each commitment (`commitment-device`). diff --git a/docs/devices/overview.md b/docs/devices/overview.md index 5a9ce0d1b..d659304de 100644 --- a/docs/devices/overview.md +++ b/docs/devices/overview.md @@ -16,6 +16,7 @@ Below is a list of documented built-in devices. Each page details the device's p * **[`~lua@5.3a`](./lua-at-5-3a.md):** Lua script execution engine. * **[`~relay@1.0`](./relay-at-1-0.md):** Relaying messages to other nodes or HTTP endpoints. * **[`~json@1.0`](./json-at-1-0.md):** Provides access to JSON data structures using HyperPATHs. +* **[`~recorder@1.0`](./recorder-at-1-0.md):** Process-local flight recorder for AO-Core event telemetry. *(More devices will be documented here as specifications are finalized and reviewed.)* diff --git a/docs/devices/recorder-at-1-0.md b/docs/devices/recorder-at-1-0.md new file mode 100644 index 000000000..c31e15379 --- /dev/null +++ b/docs/devices/recorder-at-1-0.md @@ -0,0 +1,58 @@ +# Device: ~recorder@1.0 + +The `~recorder@1.0` device records process-local HyperBEAM event telemetry for +one AO-Core resolution and renders it as a flight recorder report. + +## Chained Flight + +Use `take-off` before the path you want to inspect, then end the path with +`land~recorder@1.0`: + +```text +GET /~recorder@1.0/take-off/~meta@1.0/info/land~recorder@1.0 +``` + +`take-off` starts recording in the current HTTP evaluation process and passes +the base message onward. `land` returns the captured flight and clears recorder +state. + +For a relayed HTTP call: + +```text +GET /~recorder@1.0/take-off/call~relay@1.0&relay-method=GET&relay-path=https%3A%2F%2Ficanhazip.com/land~recorder@1.0 +``` + +## Formats + +`land` and `record` default to `format=html`. + +Use `format=raw` for the AO list of event messages: + +```text +GET /~recorder@1.0/take-off/~meta@1.0/info/land~recorder@1.0&format=raw +``` + +Other supported formats are `json` and `text`. + +## Stacks + +Stack capture is off by default. Add `stack=true` to `take-off` or `record` +when you need stack traces: + +```text +GET /~recorder@1.0/take-off&stack=true/~meta@1.0/info/land~recorder@1.0 +``` + +`trace=true` is accepted as an alias for `stack=true`. + +## Single-call Form + +`record` wraps a target request and returns the rendered report directly: + +```text +GET /~recorder@1.0/record?request=/~meta@1.0/info +``` + +This is useful when a client wants the recorder to perform the target request +itself, rather than composing a HyperPATH with explicit `take-off` and `land` +segments. diff --git a/docs/introduction/ao-devices.md b/docs/introduction/ao-devices.md index 818e0d42d..fda6e81aa 100644 --- a/docs/introduction/ao-devices.md +++ b/docs/introduction/ao-devices.md @@ -29,9 +29,9 @@ HyperBEAM includes many preloaded devices that provide core functionality. Some Devices aren't limited to just computation or state management. They can represent more abstract concepts: -* **Security Devices ([`~snp@1.0`](../resources/source-code/dev_snp.md), [`dev_codec_httpsig`](../resources/source-code/dev_codec_httpsig.md)):** Handle tasks related to Trusted Execution Environments (TEEs) or message signing, adding layers of security and verification. +* **Security Devices ([`~snp@1.0`](../resources/source-code/dev_snp.md), [`dev_httpsig`](../resources/source-code/dev_httpsig.md)):** Handle tasks related to Trusted Execution Environments (TEEs) or message signing, adding layers of security and verification. * **Payment/Access Control Devices ([`~p4@1.0`](../resources/source-code/dev_p4.md), [`~faff@1.0`](../resources/source-code/dev_faff.md)):** Manage metering, billing, or access control for node services. -* **Workflow/Utility Devices ([`dev_cron`](../resources/source-code/dev_cron.md), [`dev_stack`](../resources/source-code/dev_stack.md), [`dev_monitor`](../resources/source-code/dev_monitor.md)):** Coordinate complex execution flows, schedule tasks, or monitor process activity. +* **Workflow/Utility Devices ([`dev_cron`](../resources/source-code/dev_cron.md), [`dev_stack`](../resources/source-code/dev_stack.md)):** Coordinate complex execution flows or schedule tasks. ## Using Devices diff --git a/docs/introduction/pathing-in-ao-core.md b/docs/introduction/pathing-in-ao-core.md index 2c1a82be4..28593ea42 100644 --- a/docs/introduction/pathing-in-ao-core.md +++ b/docs/introduction/pathing-in-ao-core.md @@ -53,7 +53,7 @@ This shows the 'cache' of your process. Each response is: Beyond path segments, HyperBEAM URLs can include query parameters that utilize a special type casting syntax. This allows specifying the desired data type for a parameter directly within the URL using the format `key+type=value`. - **Syntax**: A `+` symbol separates the parameter key from its intended type (e.g., `count+integer=42`, `items+list="apple",7`). -- **Mechanism**: The HyperBEAM node identifies the `+type` suffix (e.g., `+integer`, `+list`, `+map`, `+float`, `+atom`, `+resolve`). It then uses internal functions ([`hb_singleton:maybe_typed`](../resources/source-code/hb_singleton.md) and [`dev_codec_structured:decode_value`](../resources/source-code/dev_codec_structured.md)) to decode and cast the provided value string into the corresponding Erlang data type before incorporating it into the message. +- **Mechanism**: The HyperBEAM node identifies the `+type` suffix (e.g., `+integer`, `+list`, `+map`, `+float`, `+atom`, `+resolve`). It then uses internal functions ([`hb_singleton:maybe_typed`](../resources/source-code/hb_singleton.md) and [`dev_structured:decode_value`](../resources/source-code/dev_structured.md)) to decode and cast the provided value string into the corresponding Erlang data type before incorporating it into the message. - **Supported Types**: Common types include `integer`, `float`, `list`, `map`, `atom`, `binary` (often implicit), and `resolve` (for path resolution). List values often follow the [HTTP Structured Fields format (RFC 8941)](https://www.rfc-editor.org/rfc/rfc8941.html). This powerful feature enables the expression of complex data structures directly in URLs. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 29c722720..68f0a256c 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -128,7 +128,7 @@ A Device is essentially an Erlang module (typically named `dev_*.erl`) that proc **Steps:** 1. **Define Purpose:** Clearly define what your device will do. What kind of messages will it process? What state will it manage (if any)? What functions (keys) will it expose? -2. **Create Module:** Create a new Erlang module (e.g., `src/dev_my_new_device.erl`). +2. **Create Module:** Create a new Erlang module (e.g., `src/preloaded/dev_my_new_device.erl`). 3. **Implement `info/0..2` (Optional but Recommended):** Define an `info` function to signal capabilities and requirements to HyperBEAM (e.g., exported keys, variant/version ID). ```erlang info() -> @@ -363,7 +363,7 @@ WebAssembly (WASM) allows you to run precompiled code written in languages like [aos]> Send({ Target = "AOS", Action = "Spawn", Module = "", Scheduler = "" }) -- This returns a ``` - *(Note: The exact spawning mechanism might vary; consult `aos` or relevant SDK documentation. You specify that this process uses WASM.)* + *(Note: The exact spawning mechanism might vary; consult `aos` or relevant Forge documentation. You specify that this process uses WASM.)* 4. **Send Message to Trigger Execution:** ```lua [aos]> Send({ Target = "", Action = "ExecuteFunction", InputData = "Some data for WASM" }) @@ -412,7 +412,7 @@ If a HyperBEAM node performing these computations runs within a supported Truste This usually involves interacting with nodes specifically advertised as TEE-enabled. The exact mechanism for requesting and verifying attestations depends on the specific TEE technology and node configuration. -* The HTTP response headers might contain specific signature or attestation data (e.g., using HTTP Message Signatures RFC-9421 via [`dev_codec_httpsig`](../resources/source-code/dev_codec_httpsig.md)). +* The HTTP response headers might contain specific signature or attestation data (e.g., using HTTP Message Signatures RFC-9421 via [`dev_httpsig`](../resources/source-code/dev_httpsig.md)). * You might query the [`~snp@1.0`](../resources/source-code/dev_snp.md) device directly on the node to get its attestation report. Refer to documentation on [TEE Nodes](./run/tee-nodes.md) and the [`~snp@1.0`](../resources/source-code/dev_snp.md) device for details. @@ -463,7 +463,7 @@ This retrieves the node configuration from the meta device and serializes it to - [Message Device](../resources/source-code/dev_message.md) - Works well with JSON serialization - [Meta Device](../resources/source-code/dev_meta.md) - Can provide configuration data to serialize -[json module](../resources/source-code/dev_codec_json.md) +[json module](../resources/source-code/dev_json.md) --- END OF FILE: docs/devices/json-at-1-0.md --- --- START OF FILE: docs/devices/lua-at-5-3a.md --- @@ -552,7 +552,7 @@ This device is particularly useful for: * Creating and modifying transient messages on the fly using query parameters. * Retrieving specific values from a message map. * Inspecting the keys of a message. -* Handling message commitments and verification (though often delegated to specialized commitment devices like [`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). +* Handling message commitments and verification (though often delegated to specialized commitment devices like [`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). ## Core Functionality @@ -586,8 +586,8 @@ The `message@1.0` device reserves several keys for specific operations: * **`set_path`**: A special case for setting the `path` key itself, which cannot be done via the standard `set` operation. * **`remove`**: Removes one or more specified keys from the message. Requires an `item` or `items` parameter. * **`keys`**: Returns a list of all public (non-private) keys present in the message map. -* **`id`**: Calculates and returns the ID (hash) of the message. Considers active commitments based on specified `committers`. May delegate ID calculation to a device specified by the message\'s `id-device` key or the default ([`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). -* **`commit`**: Creates a commitment (e.g., a signature) for the message. Requires parameters like `commitment-device` and potentially committer information. Delegates the actual commitment generation to the specified device (default [`httpsig@1.0`](../resources/source-code/dev_codec_httpsig.md)). +* **`id`**: Calculates and returns the ID (hash) of the message. Considers active commitments based on specified `committers`. May delegate ID calculation to a device specified by the message\'s `id-device` key or the default ([`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). +* **`commit`**: Creates a commitment (e.g., a signature) for the message. Requires parameters like `commitment-device` and potentially committer information. Delegates the actual commitment generation to the specified device (default [`httpsig@1.0`](../resources/source-code/dev_httpsig.md)). * **`committers`**: Returns a list of committers associated with the commitments in the message. Can be filtered by request parameters. * **`commitments`**: Used internally and in requests to filter or specify which commitments to operate on (e.g., for `id` or `verify`). * **`verify`**: Verifies the commitments attached to the message. Can be filtered by `committers` or specific `commitment` IDs in the request. Delegates verification to the device specified in each commitment (`commitment-device`). @@ -999,9 +999,9 @@ HyperBEAM includes many preloaded devices that provide core functionality. Some Devices aren't limited to just computation or state management. They can represent more abstract concepts: -* **Security Devices ([`~snp@1.0`](../resources/source-code/dev_snp.md), [`dev_codec_httpsig`](../resources/source-code/dev_codec_httpsig.md)):** Handle tasks related to Trusted Execution Environments (TEEs) or message signing, adding layers of security and verification. +* **Security Devices ([`~snp@1.0`](../resources/source-code/dev_snp.md), [`dev_httpsig`](../resources/source-code/dev_httpsig.md)):** Handle tasks related to Trusted Execution Environments (TEEs) or message signing, adding layers of security and verification. * **Payment/Access Control Devices ([`~p4@1.0`](../resources/source-code/dev_p4.md), [`~faff@1.0`](../resources/source-code/dev_faff.md)):** Manage metering, billing, or access control for node services. -* **Workflow/Utility Devices ([`dev_cron`](../resources/source-code/dev_cron.md), [`dev_stack`](../resources/source-code/dev_stack.md), [`dev_monitor`](../resources/source-code/dev_monitor.md)):** Coordinate complex execution flows, schedule tasks, or monitor process activity. +* **Workflow/Utility Devices ([`dev_cron`](../resources/source-code/dev_cron.md), [`dev_stack`](../resources/source-code/dev_stack.md)):** Coordinate complex execution flows or schedule tasks. ## Using Devices @@ -1087,7 +1087,7 @@ This shows the 'cache' of your process. Each response is: Beyond path segments, HyperBEAM URLs can include query parameters that utilize a special type casting syntax. This allows specifying the desired data type for a parameter directly within the URL using the format `key+type=value`. - **Syntax**: A `+` symbol separates the parameter key from its intended type (e.g., `count+integer=42`, `items+list="apple",7`). -- **Mechanism**: The HyperBEAM node identifies the `+type` suffix (e.g., `+integer`, `+list`, `+map`, `+float`, `+atom`, `+resolve`). It then uses internal functions ([`hb_singleton:maybe_typed`](../resources/source-code/hb_singleton.md) and [`dev_codec_structured:decode_value`](../resources/source-code/dev_codec_structured.md)) to decode and cast the provided value string into the corresponding Erlang data type before incorporating it into the message. +- **Mechanism**: The HyperBEAM node identifies the `+type` suffix (e.g., `+integer`, `+list`, `+map`, `+float`, `+atom`, `+resolve`). It then uses internal functions ([`hb_singleton:maybe_typed`](../resources/source-code/hb_singleton.md) and [`dev_structured:decode_value`](../resources/source-code/dev_structured.md)) to decode and cast the provided value string into the corresponding Erlang data type before incorporating it into the message. - **Supported Types**: Common types include `integer`, `float`, `list`, `map`, `atom`, `binary` (often implicit), and `resolve` (for path resolution). List values often follow the [HTTP Structured Fields format (RFC 8941)](https://www.rfc-editor.org/rfc/rfc8941.html). This powerful feature enables the expression of complex data structures directly in URLs. @@ -2754,7 +2754,7 @@ Verify that a signature is correct. --- END OF FILE: docs/resources/source-code/ar_wallet.md --- --- START OF FILE: docs/resources/source-code/dev_cache.md --- -# [Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cache.erl) +# [Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cache.erl) @@ -2888,7 +2888,7 @@ a successful write. --- END OF FILE: docs/resources/source-code/dev_cache.md --- --- START OF FILE: docs/resources/source-code/dev_cacheviz.md --- -# [Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cacheviz.erl) +# [Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cacheviz.erl) @@ -2931,8 +2931,8 @@ the cache set by the `target` key in the request. --- END OF FILE: docs/resources/source-code/dev_cacheviz.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_ans104.md --- -# [Module dev_codec_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_ans104.erl) +--- START OF FILE: docs/resources/source-code/dev_ans104.md --- +# [Module dev_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_ans104.erl) @@ -3132,10 +3132,10 @@ a binary, which we return as is. Verify an ANS-104 commitment. ---- END OF FILE: docs/resources/source-code/dev_codec_ans104.md --- +--- END OF FILE: docs/resources/source-code/dev_ans104.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_flat.md --- -# [Module dev_codec_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_flat.erl) +--- START OF FILE: docs/resources/source-code/dev_flat.md --- +# [Module dev_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_flat.erl) @@ -3251,10 +3251,10 @@ Convert a TABM to a flat map. `verify(Msg, Req, Opts) -> any()` ---- END OF FILE: docs/resources/source-code/dev_codec_flat.md --- +--- END OF FILE: docs/resources/source-code/dev_flat.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_httpsig_conv.md --- -# [Module dev_codec_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig_conv.erl) +--- START OF FILE: docs/resources/source-code/dev_httpsig_conv.md --- +# [Module dev_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig_conv.erl) @@ -3496,10 +3496,10 @@ that can translated to a given web server Response API Decode the `ao-ids` key into a map. ---- END OF FILE: docs/resources/source-code/dev_codec_httpsig_conv.md --- +--- END OF FILE: docs/resources/source-code/dev_httpsig_conv.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_httpsig.md --- -# [Module dev_codec_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig.erl) +--- START OF FILE: docs/resources/source-code/dev_httpsig.md --- +# [Module dev_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig.erl) @@ -3513,7 +3513,7 @@ This module implements HTTP Message Signatures as described in RFC-9421 It implements the codec standard (from/1, to/1), as well as the optional commitment functions (id/3, sign/3, verify/3). The commitment functions are found in this module, while the codec functions are relayed to the -`dev_codec_httpsig_conv` module. +`dev_httpsig_conv` module. ## Data Types ## @@ -4136,10 +4136,10 @@ Given the signature name, and the Request/Response Message Context verify the named signature by constructing the signature base and comparing ---- END OF FILE: docs/resources/source-code/dev_codec_httpsig.md --- +--- END OF FILE: docs/resources/source-code/dev_httpsig.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_json.md --- -# [Module dev_codec_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_json.erl) +--- START OF FILE: docs/resources/source-code/dev_json.md --- +# [Module dev_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json.erl) @@ -4222,10 +4222,10 @@ Encode a message to a JSON string. `verify(Msg, Req, Opts) -> any()` ---- END OF FILE: docs/resources/source-code/dev_codec_json.md --- +--- END OF FILE: docs/resources/source-code/dev_json.md --- ---- START OF FILE: docs/resources/source-code/dev_codec_structured.md --- -# [Module dev_codec_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_structured.erl) +--- START OF FILE: docs/resources/source-code/dev_structured.md --- +# [Module dev_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_structured.erl) @@ -4332,10 +4332,10 @@ Convert a TABM into a native HyperBEAM message. `verify(Msg, Req, Opts) -> any()` ---- END OF FILE: docs/resources/source-code/dev_codec_structured.md --- +--- END OF FILE: docs/resources/source-code/dev_structured.md --- --- START OF FILE: docs/resources/source-code/dev_cron.md --- -# [Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cron.erl) +# [Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cron.erl) @@ -4465,41 +4465,8 @@ It is used to increment a counter and update the state of the worker. --- END OF FILE: docs/resources/source-code/dev_cron.md --- ---- START OF FILE: docs/resources/source-code/dev_cu.md --- -# [Module dev_cu.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cu.erl) - - - - - - -## Function Index ## - - -
execute/2
push/2
- - - - -## Function Details ## - - - -### execute/2 ### - -`execute(CarrierMsg, S) -> any()` - - - -### push/2 ### - -`push(Msg, S) -> any()` - - ---- END OF FILE: docs/resources/source-code/dev_cu.md --- - --- START OF FILE: docs/resources/source-code/dev_dedup.md --- -# [Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_dedup.erl) +# [Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_dedup.erl) @@ -4556,7 +4523,7 @@ with deduplication. We only act on the first pass. --- END OF FILE: docs/resources/source-code/dev_dedup.md --- --- START OF FILE: docs/resources/source-code/dev_delegated_compute.md --- -# [Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_delegated_compute.erl) +# [Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_delegated_compute.erl) @@ -4620,7 +4587,7 @@ need to do anything special here. --- END OF FILE: docs/resources/source-code/dev_delegated_compute.md --- --- START OF FILE: docs/resources/source-code/dev_faff.md --- -# [Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_faff.erl) +# [Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_faff.erl) @@ -4678,7 +4645,7 @@ Check whether all of the signers of the request are in the allow-list. --- END OF FILE: docs/resources/source-code/dev_faff.md --- --- START OF FILE: docs/resources/source-code/dev_genesis_wasm.md --- -# [Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_genesis_wasm.erl) +# [Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_genesis_wasm.erl) @@ -4790,7 +4757,7 @@ endpoint. --- END OF FILE: docs/resources/source-code/dev_genesis_wasm.md --- --- START OF FILE: docs/resources/source-code/dev_green_zone.md --- -# [Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_green_zone.erl) +# [Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_green_zone.erl) @@ -4809,7 +4776,7 @@ commitment and encryption.
add_trusted_node/4*Adds a node to the trusted nodes list with its commitment report.
become/3Clones the identity of a target node in the green zone.
calculate_node_message/3*Generate the node message that should be set prior to joining -a green zone.
decrypt_zone_key/2*Decrypts an AES key using the node's RSA private key.
default_zone_required_opts/1*Provides the default required options for a green zone.
encrypt_payload/2*Encrypts an AES key with a node's RSA public key.
finalize_become/5*
info/1Controls which functions are exposed via the device API.
info/3Provides information about the green zone device and its API.
init/3Initialize the green zone for a node.
join/3Initiates the join process for a node to enter an existing green zone.
join_peer/5*Processes a join request to a specific peer node.
key/3Encrypts and provides the node's private key for secure sharing.
maybe_set_zone_opts/4*Adopts configuration from a peer when joining a green zone.
rsa_wallet_integration_test/0*Test RSA operations with the existing wallet structure.
try_mount_encrypted_volume/2*Attempts to mount an encrypted volume using the green zone AES key.
validate_join/3*Validates an incoming join request from another node.
validate_peer_opts/2*Validates that a peer's configuration matches required options.
+a green zone.decrypt_zone_key/2*Decrypts an AES key using the node's RSA private key.default_zone_required_opts/1*Provides the default required options for a green zone.encrypt_payload/2*Encrypts an AES key with a node's RSA public key.finalize_become/5*info/1Controls which functions are exposed via the device API.info/3Provides information about the green zone device and its API.init/3Initialize the green zone for a node.join/3Initiates the join process for a node to enter an existing green zone.join_peer/5*Processes a join request to a specific peer node.key/3Encrypts and provides the node's private key for secure sharing.maybe_set_zone_opts/4*Adopts configuration from a peer when joining a green zone.rsa_wallet_integration_test/0*Test RSA operations with the existing wallet structure.validate_join/3*Validates an incoming join request from another node.validate_peer_opts/2*Validates that a peer's configuration matches required options. @@ -5044,7 +5011,7 @@ join_peer(PeerLocation::binary(), PeerID::binary(), M1::term(), M2::term(), Opts
-`PeerLocation`: The target peer's address
`PeerID`: The target peer's unique identifier
`M2`: May contain ShouldMount flag to enable encrypted volume mounting
+`PeerLocation`: The target peer's address
`PeerID`: The target peer's unique identifier
`M2`: Additional request details
returns: `{ok, Map}` on success with confirmation message, or `{error, Map|Binary}` on failure with error details @@ -5059,7 +5026,6 @@ This function handles the client-side join flow when connecting to a peer: 5. Verifies the response signature 6. Decrypts the returned AES key 7. Updates local configuration with the shared key -8. Optionally mounts an encrypted volume using the shared key @@ -5127,21 +5093,6 @@ from the wallet work correctly. It creates a new wallet, encrypts a test message with the RSA public key, and then decrypts it with the RSA private key, asserting that the decrypted message matches the original. - - -### try_mount_encrypted_volume/2 * ### - -`try_mount_encrypted_volume(AESKey, Opts) -> any()` - -Attempts to mount an encrypted volume using the green zone AES key. - -This function handles the complete process of secure storage setup by -delegating to the dev_volume module, which provides a unified interface -for volume management. - -The encryption key used for the volume is the same AES key used for green zone -communication, ensuring that only nodes in the green zone can access the data. - ### validate_join/3 * ### @@ -5193,7 +5144,7 @@ This function ensures the peer node meets configuration requirements: --- END OF FILE: docs/resources/source-code/dev_green_zone.md --- --- START OF FILE: docs/resources/source-code/dev_hook.md --- -# [Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hook.erl) +# [Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hook.erl) @@ -5363,7 +5314,7 @@ Test that a single handler is executed correctly --- END OF FILE: docs/resources/source-code/dev_hook.md --- --- START OF FILE: docs/resources/source-code/dev_hyperbuddy.md --- -# [Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hyperbuddy.erl) +# [Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hyperbuddy.erl) @@ -5427,7 +5378,7 @@ listed in the `routes` field of the `info/0` return value. --- END OF FILE: docs/resources/source-code/dev_hyperbuddy.md --- --- START OF FILE: docs/resources/source-code/dev_json_iface.md --- -# [Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_json_iface.erl) +# [Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json_iface.erl) @@ -5685,7 +5636,7 @@ Convert a message with tags into a map of their key-value pairs. --- END OF FILE: docs/resources/source-code/dev_json_iface.md --- --- START OF FILE: docs/resources/source-code/dev_local_name.md --- -# [Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_local_name.erl) +# [Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_local_name.erl) @@ -5818,64 +5769,8 @@ storage. --- END OF FILE: docs/resources/source-code/dev_local_name.md --- ---- START OF FILE: docs/resources/source-code/dev_lookup.md --- -# [Module dev_lookup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lookup.erl) - - - - -A device that looks up an ID from a local store and returns it, honoring -the `accept` key to return the correct format. - - - -## Function Index ## - - -
aos2_message_lookup_test/0*
binary_lookup_test/0*
http_lookup_test/0*
message_lookup_test/0*
read/3Fetch a resource from the cache using "target" ID extracted from the message.
- - - - -## Function Details ## - - - -### aos2_message_lookup_test/0 * ### - -`aos2_message_lookup_test() -> any()` - - - -### binary_lookup_test/0 * ### - -`binary_lookup_test() -> any()` - - - -### http_lookup_test/0 * ### - -`http_lookup_test() -> any()` - - - -### message_lookup_test/0 * ### - -`message_lookup_test() -> any()` - - - -### read/3 ### - -`read(M1, M2, Opts) -> any()` - -Fetch a resource from the cache using "target" ID extracted from the message - - ---- END OF FILE: docs/resources/source-code/dev_lookup.md --- - --- START OF FILE: docs/resources/source-code/dev_lua_lib.md --- -# [Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_lib.erl) +# [Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_lib.erl) @@ -5965,7 +5860,7 @@ Wrapper for `hb_ao`'s `set` functionality. --- END OF FILE: docs/resources/source-code/dev_lua_lib.md --- --- START OF FILE: docs/resources/source-code/dev_lua_test.md --- -# [Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_test.erl) +# [Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_test.erl) @@ -6061,7 +5956,7 @@ Check if a string terminates with a given suffix. --- END OF FILE: docs/resources/source-code/dev_lua_test.md --- --- START OF FILE: docs/resources/source-code/dev_lua.md --- -# [Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua.erl) +# [Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua.erl) @@ -6370,7 +6265,7 @@ state element, then serializes the state to a binary. --- END OF FILE: docs/resources/source-code/dev_lua.md --- --- START OF FILE: docs/resources/source-code/dev_manifest.md --- -# [Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_manifest.erl) +# [Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_manifest.erl) @@ -6423,7 +6318,7 @@ Route a request to the associated data via its manifest. --- END OF FILE: docs/resources/source-code/dev_manifest.md --- --- START OF FILE: docs/resources/source-code/dev_message.md --- -# [Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_message.erl) +# [Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_message.erl) @@ -6752,7 +6647,7 @@ See `commitment_ids_from_request/3` for more information on the request format. --- END OF FILE: docs/resources/source-code/dev_message.md --- --- START OF FILE: docs/resources/source-code/dev_meta.md --- -# [Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_meta.erl) +# [Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_meta.erl) @@ -7047,65 +6942,8 @@ allow them to update the node message. --- END OF FILE: docs/resources/source-code/dev_meta.md --- ---- START OF FILE: docs/resources/source-code/dev_monitor.md --- -# [Module dev_monitor.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_monitor.erl) - - - - - - -## Function Index ## - - -
add_monitor/2
end_of_schedule/1
execute/2
init/3
signal/2*
uses/0
- - - - -## Function Details ## - - - -### add_monitor/2 ### - -`add_monitor(Mon, State) -> any()` - - - -### end_of_schedule/1 ### - -`end_of_schedule(State) -> any()` - - - -### execute/2 ### - -`execute(Message, State) -> any()` - - - -### init/3 ### - -`init(State, X2, InitState) -> any()` - - - -### signal/2 * ### - -`signal(State, Signal) -> any()` - - - -### uses/0 ### - -`uses() -> any()` - - ---- END OF FILE: docs/resources/source-code/dev_monitor.md --- - --- START OF FILE: docs/resources/source-code/dev_multipass.md --- -# [Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_multipass.erl) +# [Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_multipass.erl) @@ -7155,7 +6993,7 @@ with deduplication. We only act on the first pass. --- END OF FILE: docs/resources/source-code/dev_multipass.md --- --- START OF FILE: docs/resources/source-code/dev_name.md --- -# [Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_name.erl) +# [Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_name.erl) @@ -7256,7 +7094,7 @@ pointer and its contents is loaded from the cache. For example, --- END OF FILE: docs/resources/source-code/dev_name.md --- --- START OF FILE: docs/resources/source-code/dev_node_process.md --- -# [Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_node_process.erl) +# [Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_node_process.erl) @@ -7357,7 +7195,7 @@ node message, and register it with the given name. --- END OF FILE: docs/resources/source-code/dev_node_process.md --- --- START OF FILE: docs/resources/source-code/dev_p4.md --- -# [Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_p4.erl) +# [Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_p4.erl) @@ -7500,7 +7338,7 @@ Postprocess the request after it has been fulfilled. --- END OF FILE: docs/resources/source-code/dev_p4.md --- --- START OF FILE: docs/resources/source-code/dev_patch.md --- -# [Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_patch.erl) +# [Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_patch.erl) @@ -7629,7 +7467,7 @@ request. --- END OF FILE: docs/resources/source-code/dev_patch.md --- --- START OF FILE: docs/resources/source-code/dev_poda.md --- -# [Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_poda.erl) +# [Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_poda.erl) @@ -7762,7 +7600,7 @@ outbound message if the computation requests it. --- END OF FILE: docs/resources/source-code/dev_poda.md --- --- START OF FILE: docs/resources/source-code/dev_process_cache.md --- -# [Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_cache.erl) +# [Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_cache.erl) @@ -7883,7 +7721,7 @@ Write a process computation result to the cache. --- END OF FILE: docs/resources/source-code/dev_process_cache.md --- --- START OF FILE: docs/resources/source-code/dev_process_worker.md --- -# [Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_worker.erl) +# [Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_worker.erl) @@ -7991,7 +7829,7 @@ Stop a worker process. --- END OF FILE: docs/resources/source-code/dev_process_worker.md --- --- START OF FILE: docs/resources/source-code/dev_process.md --- -# [Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process.erl) +# [Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process.erl) @@ -8448,7 +8286,7 @@ executor. --- END OF FILE: docs/resources/source-code/dev_process.md --- --- START OF FILE: docs/resources/source-code/dev_push.md --- -# [Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_push.erl) +# [Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_push.erl) @@ -8648,7 +8486,7 @@ Find the target process ID for a message to push. --- END OF FILE: docs/resources/source-code/dev_push.md --- --- START OF FILE: docs/resources/source-code/dev_relay.md --- -# [Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_relay.erl) +# [Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_relay.erl) @@ -8736,7 +8574,7 @@ peers, according to the node's routing table. --- END OF FILE: docs/resources/source-code/dev_relay.md --- --- START OF FILE: docs/resources/source-code/dev_router.md --- -# [Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_router.erl) +# [Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_router.erl) @@ -9171,7 +9009,7 @@ Check if a message matches a message template or path regex. --- END OF FILE: docs/resources/source-code/dev_router.md --- --- START OF FILE: docs/resources/source-code/dev_scheduler_cache.md --- -# [Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_cache.erl) +# [Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_cache.erl) @@ -9240,7 +9078,7 @@ Write the latest known scheduler location for an address. --- END OF FILE: docs/resources/source-code/dev_scheduler_cache.md --- --- START OF FILE: docs/resources/source-code/dev_scheduler_formats.md --- -# [Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_formats.erl) +# [Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_formats.erl) @@ -9364,7 +9202,7 @@ perform cache lookups, or await inprogress results. --- END OF FILE: docs/resources/source-code/dev_scheduler_formats.md --- --- START OF FILE: docs/resources/source-code/dev_scheduler_registry.md --- -# [Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_registry.erl) +# [Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_registry.erl) @@ -9463,7 +9301,7 @@ Return a list of all currently registered ProcID. --- END OF FILE: docs/resources/source-code/dev_scheduler_registry.md --- --- START OF FILE: docs/resources/source-code/dev_scheduler_server.md --- -# [Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_server.erl) +# [Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_server.erl) @@ -9569,7 +9407,7 @@ Start a scheduling server for a given computation. --- END OF FILE: docs/resources/source-code/dev_scheduler_server.md --- --- START OF FILE: docs/resources/source-code/dev_scheduler.md --- -# [Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler.erl) +# [Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler.erl) @@ -10125,7 +9963,7 @@ process ID. --- END OF FILE: docs/resources/source-code/dev_scheduler.md --- --- START OF FILE: docs/resources/source-code/dev_simple_pay.md --- -# [Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_simple_pay.erl) +# [Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_simple_pay.erl) @@ -10228,8 +10066,8 @@ Top up the user's balance in the ledger. --- END OF FILE: docs/resources/source-code/dev_simple_pay.md --- ---- START OF FILE: docs/resources/source-code/dev_snp_nif.md --- -# [Module dev_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp_nif.erl) +--- START OF FILE: docs/resources/source-code/hb_snp_nif.md --- +# [Module hb_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/core/hb_snp_nif.erl) @@ -10313,10 +10151,10 @@ Top up the user's balance in the ledger. `verify_signature_test() -> any()` ---- END OF FILE: docs/resources/source-code/dev_snp_nif.md --- +--- END OF FILE: docs/resources/source-code/hb_snp_nif.md --- --- START OF FILE: docs/resources/source-code/dev_snp.md --- -# [Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp.erl) +# [Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_snp.erl) @@ -10424,7 +10262,7 @@ measurement, are trusted. --- END OF FILE: docs/resources/source-code/dev_snp.md --- --- START OF FILE: docs/resources/source-code/dev_stack.md --- -# [Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_stack.erl) +# [Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_stack.erl) @@ -10788,7 +10626,7 @@ keyInDevice executed on DeviceName against Msg1. --- END OF FILE: docs/resources/source-code/dev_stack.md --- --- START OF FILE: docs/resources/source-code/dev_test.md --- -# [Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_test.erl) +# [Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_test.erl) @@ -10925,283 +10763,9 @@ Find a test worker's PID and send it an update message. --- END OF FILE: docs/resources/source-code/dev_test.md --- ---- START OF FILE: docs/resources/source-code/dev_volume.md --- -# [Module dev_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_volume.erl) - - - - -Secure Volume Management for HyperBEAM Nodes. - - - -## Description ## - -This module handles encrypted storage operations for HyperBEAM, providing -a robust and secure approach to data persistence. It manages the complete -lifecycle of encrypted volumes from detection to creation, formatting, and -mounting. - -Key responsibilities: -- Volume detection and initialization -- Encrypted partition creation and formatting -- Secure mounting using cryptographic keys -- Store path reconfiguration to use mounted volumes -- Automatic handling of various system states -(new device, existing partition, etc.) - -The primary entry point is the `mount/3` function, which orchestrates the -entire process based on the provided configuration parameters. This module -works alongside `hb_volume` which provides the low-level operations for -device manipulation. - -Security considerations: -- Ensures data at rest is protected through LUKS encryption -- Provides proper volume sanitization and secure mounting -- IMPORTANT: This module only applies configuration set in node options and -does NOT accept disk operations via HTTP requests. It cannot format arbitrary -disks as all operations are safeguarded by host operating system permissions -enforced upon the HyperBEAM environment. - -## Function Index ## - - -
check_base_device/8*Check if the base device exists and if it does, check if the partition exists.
check_partition/8*Check if the partition exists.
create_and_mount_partition/8*Create, format and mount a new partition.
decrypt_volume_key/2*Decrypts an encrypted volume key using the node's private key.
format_and_mount/6*Format and mount a newly created partition.
info/1Exported function for getting device info, controls which functions are -exposed via the device API.
info/3HTTP info response providing information about this device.
mount/3Handles the complete process of secure encrypted volume mounting.
mount_existing_partition/6*Mount an existing partition.
mount_formatted_partition/6*Mount a newly formatted partition.
public_key/3Returns the node's public key for secure key exchange.
update_node_config/2*Update the node's configuration with the new store.
update_store_path/2*Update the store path to use the mounted volume.
- - - - -## Function Details ## - - - -### check_base_device/8 * ### - -

-check_base_device(Device::term(), Partition::term(), PartitionType::term(), VolumeName::term(), MountPoint::term(), StorePath::term(), Key::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The base device to check.
`Partition`: The partition to check.
`PartitionType`: The type of partition to check.
`VolumeName`: The name of the volume to check.
`MountPoint`: The mount point to check.
`StorePath`: The store path to check.
`Key`: The key to check.
`Opts`: The options to check.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Check if the base device exists and if it does, check if the partition exists. - - - -### check_partition/8 * ### - -

-check_partition(Device::term(), Partition::term(), PartitionType::term(), VolumeName::term(), MountPoint::term(), StorePath::term(), Key::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The base device to check.
`Partition`: The partition to check.
`PartitionType`: The type of partition to check.
`VolumeName`: The name of the volume to check.
`MountPoint`: The mount point to check.
`StorePath`: The store path to check.
`Key`: The key to check.
`Opts`: The options to check.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Check if the partition exists. If it does, attempt to mount it. -If it doesn't exist, create it, format it with encryption and mount it. - - - -### create_and_mount_partition/8 * ### - -

-create_and_mount_partition(Device::term(), Partition::term(), PartitionType::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The device to create the partition on.
`Partition`: The partition to create.
`PartitionType`: The type of partition to create.
`Key`: The key to create the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Create, format and mount a new partition. - - - -### decrypt_volume_key/2 * ### - -

-decrypt_volume_key(EncryptedKeyBase64::binary(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Opts`: A map of configuration options.
- -returns: `{ok, DecryptedKey}` on successful decryption, or -`{error, Binary}` if decryption fails. - -Decrypts an encrypted volume key using the node's private key. - -This function takes an encrypted key (typically sent by a client who encrypted -it with the node's public key) and decrypts it using the node's private RSA key. - - - -### format_and_mount/6 * ### - -

-format_and_mount(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to format and mount.
`Key`: The key to format and mount the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Format and mount a newly created partition. - - - -### info/1 ### - -`info(X1) -> any()` - -Exported function for getting device info, controls which functions are -exposed via the device API. - - - -### info/3 ### - -`info(Msg1, Msg2, Opts) -> any()` - -HTTP info response providing information about this device - - - -### mount/3 ### - -

-mount(M1::term(), M2::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`M1`: Base message for context.
`M2`: Request message with operation details.
`Opts`: A map of configuration options for volume operations.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Handles the complete process of secure encrypted volume mounting. - -This function performs the following operations depending on the state: -1. Validates the encryption key is present -2. Checks if the base device exists -3. Checks if the partition exists on the device -4. If the partition exists, attempts to mount it -5. If the partition doesn't exist, creates it, formats it with encryption -and mounts it -6. Updates the node's store configuration to use the mounted volume - -Config options in Opts map: -- volume_key: (Required) The encryption key -- volume_device: Base device path -- volume_partition: Partition path -- volume_partition_type: Filesystem type -- volume_name: Name for encrypted volume -- volume_mount_point: Where to mount -- volume_store_path: Store path on volume - - - -### mount_existing_partition/6 * ### - -

-mount_existing_partition(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to mount.
`Key`: The key to mount.
`MountPoint`: The mount point to mount.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Mount an existing partition. - - - -### mount_formatted_partition/6 * ### - -

-mount_formatted_partition(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to mount.
`Key`: The key to mount the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Mount a newly formatted partition. - - - -### public_key/3 ### - -

-public_key(M1::term(), M2::term(), Opts::map()) -> {ok, map()} | {error, binary()}
-
-
- -`Opts`: A map of configuration options.
- -returns: `{ok, Map}` containing the node's public key on success, or -`{error, Binary}` if the node's wallet is not available. - -Returns the node's public key for secure key exchange. - -This function retrieves the node's wallet and extracts the public key -for encryption purposes. It allows users to securely exchange encryption keys -by first encrypting their volume key with the node's public key. - -The process ensures that sensitive keys are never transmitted in plaintext. -The encrypted key can then be securely sent to the node, which will decrypt it -using its private key before using it for volume encryption. - - - -### update_node_config/2 * ### - -

-update_node_config(NewStore::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`NewStore`: The new store to update the node's configuration with.
`Opts`: The options to update the node's configuration with.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Update the node's configuration with the new store. - - - -### update_store_path/2 * ### - -

-update_store_path(StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`StorePath`: The store path to update.
`Opts`: The options to update.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Update the store path to use the mounted volume. - - ---- END OF FILE: docs/resources/source-code/dev_volume.md --- --- START OF FILE: docs/resources/source-code/dev_wasi.md --- -# [Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasi.erl) +# [Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasi.erl) @@ -11354,7 +10918,7 @@ Return the stdout buffer from a state message. --- END OF FILE: docs/resources/source-code/dev_wasi.md --- --- START OF FILE: docs/resources/source-code/dev_wasm.md --- -# [Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasm.erl) +# [Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasm.erl) @@ -15488,16 +15052,16 @@ and O(1) access maps), such that operations upon them are efficient. The structure of the conversions is as follows:
-Arweave TX/ANS-104 ==> dev_codec_ans104:from/1 ==> TABM
-HTTP Signed Message ==> dev_codec_httpsig_conv:from/1 ==> TABM
-Flat Maps ==> dev_codec_flat:from/1 ==> TABM
+Arweave TX/ANS-104 ==> dev_ans104:from/1 ==> TABM
+HTTP Signed Message ==> dev_httpsig_conv:from/1 ==> TABM
+Flat Maps ==> dev_flat:from/1 ==> TABM
 
-TABM ==> dev_codec_structured:to/1 ==> AO-Core Message
-AO-Core Message ==> dev_codec_structured:from/1 ==> TABM
+TABM ==> dev_structured:to/1 ==> AO-Core Message
+AO-Core Message ==> dev_structured:from/1 ==> TABM
 
-TABM ==> dev_codec_ans104:to/1 ==> Arweave TX/ANS-104
-TABM ==> dev_codec_httpsig_conv:to/1 ==> HTTP Signed Message
-TABM ==> dev_codec_flat:to/1 ==> Flat Maps
+TABM ==> dev_ans104:to/1 ==> Arweave TX/ANS-104
+TABM ==> dev_httpsig_conv:to/1 ==> HTTP Signed Message
+TABM ==> dev_flat:to/1 ==> Flat Maps
 ...
 
@@ -20168,155 +19732,6 @@ Return a random element from a list, weighted by the values in the list. --- END OF FILE: docs/resources/source-code/hb_util.md --- ---- START OF FILE: docs/resources/source-code/hb_volume.md --- -# [Module hb_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_volume.erl) - - - - - - -## Function Index ## - - -
change_node_store/2
check_for_device/1
create_actual_partition/2*
create_mount_info/3*
create_partition/2
format_disk/2
get_partition_info/1*
list_partitions/0
mount_disk/4
mount_opened_volume/3*
parse_disk_info/2*
parse_disk_line/2*
parse_disk_model_line/2*
parse_disk_units_line/2*
parse_io_size_line/2*
parse_sector_size_line/2*
process_disk_line/2*
update_store_config/2*
- - - - -## Function Details ## - - - -### change_node_store/2 ### - -

-change_node_store(StorePath::binary(), CurrentStore::list()) -> {ok, map()} | {error, binary()}
-
-
- - - -### check_for_device/1 ### - -

-check_for_device(Device::binary()) -> boolean()
-
-
- - - -### create_actual_partition/2 * ### - -`create_actual_partition(Device, PartType) -> any()` - - - -### create_mount_info/3 * ### - -`create_mount_info(Partition, MountPoint, VolumeName) -> any()` - - - -### create_partition/2 ### - -

-create_partition(Device::binary(), PartType::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### format_disk/2 ### - -

-format_disk(Partition::binary(), EncKey::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### get_partition_info/1 * ### - -`get_partition_info(Device) -> any()` - - - -### list_partitions/0 ### - -

-list_partitions() -> {ok, map()} | {error, binary()}
-
-
- - - -### mount_disk/4 ### - -

-mount_disk(Partition::binary(), EncKey::binary(), MountPoint::binary(), VolumeName::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### mount_opened_volume/3 * ### - -`mount_opened_volume(Partition, MountPoint, VolumeName) -> any()` - - - -### parse_disk_info/2 * ### - -`parse_disk_info(Device, Lines) -> any()` - - - -### parse_disk_line/2 * ### - -`parse_disk_line(Line, Info) -> any()` - - - -### parse_disk_model_line/2 * ### - -`parse_disk_model_line(Line, Info) -> any()` - - - -### parse_disk_units_line/2 * ### - -`parse_disk_units_line(Line, Info) -> any()` - - - -### parse_io_size_line/2 * ### - -`parse_io_size_line(Line, Info) -> any()` - - - -### parse_sector_size_line/2 * ### - -`parse_sector_size_line(Line, Info) -> any()` - - - -### process_disk_line/2 * ### - -`process_disk_line(Line, X2) -> any()` - - - -### update_store_config/2 * ### - -

-update_store_config(StoreConfig::term(), NewPath::binary()) -> term()
-
-
- - ---- END OF FILE: docs/resources/source-code/hb_volume.md --- --- START OF FILE: docs/resources/source-code/hb.md --- # [Module hb.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb.erl) @@ -20607,7 +20022,7 @@ HyperBEAM is built with a modular architecture to ensure scalability, maintainab - **HyperBEAM Core**: The main framework that orchestrates data processing, storage, and routing. - **Compute Unit**: Handles computational tasks and integrates with the HyperBEAM core for distributed processing. - **Trusted Execution Environment (TEE)**: Ensures secure execution of sensitive operations. -- **Client Libraries**: Tools and SDKs for interacting with HyperBEAM, including the JavaScript client. +- **Client Libraries**: Tools for interacting with HyperBEAM, including the JavaScript client. ## Getting Started @@ -20638,14 +20053,13 @@ Use the navigation menu to dive into specific parts of the codebase. Each module ar_wallet dev_cache dev_cacheviz -dev_codec_ans104 -dev_codec_flat -dev_codec_httpsig -dev_codec_httpsig_conv -dev_codec_json -dev_codec_structured +dev_ans104 +dev_flat +dev_httpsig +dev_httpsig_conv +dev_json +dev_structured dev_cron -dev_cu dev_dedup dev_delegated_compute dev_faff @@ -20655,14 +20069,12 @@ Use the navigation menu to dive into specific parts of the codebase. Each module dev_hyperbuddy dev_json_iface dev_local_name -dev_lookup dev_lua dev_lua_lib dev_lua_test dev_manifest dev_message dev_meta -dev_monitor dev_multipass dev_name dev_node_process @@ -20682,10 +20094,9 @@ Use the navigation menu to dive into specific parts of the codebase. Each module dev_scheduler_server dev_simple_pay dev_snp -dev_snp_nif +hb_snp_nif dev_stack dev_test -dev_volume dev_wasi dev_wasm hb @@ -20733,7 +20144,6 @@ Use the navigation menu to dive into specific parts of the codebase. Each module hb_test_utils hb_tracer hb_util -hb_volume rsa_pss @@ -20999,7 +20409,7 @@ These options control how HyperBEAM manages devices. | Option | Type | Default | Description | |--------|------|---------|-------------| | `load_remote_devices` | Boolean | false | Whether to load devices from remote signers | - + ### Debug & Development @@ -21421,4 +20831,3 @@ Detailed documentation on the following topics will be added: If you intend to offer TEE-based computation of AO-Core devices, please see the [HyperBEAM OS repository](https://github.com/permaweb/hb-os) for preliminary details on configuration and deployment. --- END OF FILE: docs/run/tee-nodes.md --- - diff --git a/docs/llms.txt b/docs/llms.txt index 31023fbde..0f7458e57 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -52,58 +52,54 @@ Key sections include: Getting Started (begin), Running HyperBEAM (run), Develope * [[Module ar_timestamp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/ar_timestamp.erl)](./resources/source-code/ar_timestamp.html) * [[Module ar_tx.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/ar_tx.erl)](./resources/source-code/ar_tx.html) * [[Module ar_wallet.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/ar_wallet.erl)](./resources/source-code/ar_wallet.html) -* [[Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cache.erl)](./resources/source-code/dev_cache.html) -* [[Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cacheviz.erl)](./resources/source-code/dev_cacheviz.html) -* [[Module dev_codec_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_ans104.erl)](./resources/source-code/dev_codec_ans104.html) -* [[Module dev_codec_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_flat.erl)](./resources/source-code/dev_codec_flat.html) -* [[Module dev_codec_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig.erl)](./resources/source-code/dev_codec_httpsig.html) -* [[Module dev_codec_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig_conv.erl)](./resources/source-code/dev_codec_httpsig_conv.html) -* [[Module dev_codec_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_json.erl)](./resources/source-code/dev_codec_json.html) -* [[Module dev_codec_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_structured.erl)](./resources/source-code/dev_codec_structured.html) -* [[Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cron.erl)](./resources/source-code/dev_cron.html) -* [[Module dev_cu.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cu.erl)](./resources/source-code/dev_cu.html) -* [[Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_dedup.erl)](./resources/source-code/dev_dedup.html) -* [[Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_delegated_compute.erl)](./resources/source-code/dev_delegated_compute.html) -* [[Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_faff.erl)](./resources/source-code/dev_faff.html) -* [[Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_genesis_wasm.erl)](./resources/source-code/dev_genesis_wasm.html) -* [[Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_green_zone.erl)](./resources/source-code/dev_green_zone.html) -* [[Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hook.erl)](./resources/source-code/dev_hook.html) -* [[Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hyperbuddy.erl)](./resources/source-code/dev_hyperbuddy.html) -* [[Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_json_iface.erl)](./resources/source-code/dev_json_iface.html) -* [[Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_local_name.erl)](./resources/source-code/dev_local_name.html) -* [[Module dev_lookup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lookup.erl)](./resources/source-code/dev_lookup.html) -* [[Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua.erl)](./resources/source-code/dev_lua.html) -* [[Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_lib.erl)](./resources/source-code/dev_lua_lib.html) -* [[Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_test.erl)](./resources/source-code/dev_lua_test.html) -* [[Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_manifest.erl)](./resources/source-code/dev_manifest.html) -* [[Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_message.erl)](./resources/source-code/dev_message.html) -* [[Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_meta.erl)](./resources/source-code/dev_meta.html) -* [[Module dev_monitor.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_monitor.erl)](./resources/source-code/dev_monitor.html) -* [[Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_multipass.erl)](./resources/source-code/dev_multipass.html) -* [[Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_name.erl)](./resources/source-code/dev_name.html) -* [[Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_node_process.erl)](./resources/source-code/dev_node_process.html) -* [[Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_p4.erl)](./resources/source-code/dev_p4.html) -* [[Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_patch.erl)](./resources/source-code/dev_patch.html) -* [[Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_poda.erl)](./resources/source-code/dev_poda.html) -* [[Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process.erl)](./resources/source-code/dev_process.html) -* [[Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_cache.erl)](./resources/source-code/dev_process_cache.html) -* [[Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_worker.erl)](./resources/source-code/dev_process_worker.html) -* [[Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_push.erl)](./resources/source-code/dev_push.html) -* [[Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_relay.erl)](./resources/source-code/dev_relay.html) -* [[Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_router.erl)](./resources/source-code/dev_router.html) -* [[Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler.erl)](./resources/source-code/dev_scheduler.html) -* [[Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_cache.erl)](./resources/source-code/dev_scheduler_cache.html) -* [[Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_formats.erl)](./resources/source-code/dev_scheduler_formats.html) -* [[Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_registry.erl)](./resources/source-code/dev_scheduler_registry.html) -* [[Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_server.erl)](./resources/source-code/dev_scheduler_server.html) -* [[Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_simple_pay.erl)](./resources/source-code/dev_simple_pay.html) -* [[Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp.erl)](./resources/source-code/dev_snp.html) -* [[Module dev_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp_nif.erl)](./resources/source-code/dev_snp_nif.html) -* [[Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_stack.erl)](./resources/source-code/dev_stack.html) -* [[Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_test.erl)](./resources/source-code/dev_test.html) -* [[Module dev_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_volume.erl)](./resources/source-code/dev_volume.html) -* [[Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasi.erl)](./resources/source-code/dev_wasi.html) -* [[Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasm.erl)](./resources/source-code/dev_wasm.html) +* [[Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cache.erl)](./resources/source-code/dev_cache.html) +* [[Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cacheviz.erl)](./resources/source-code/dev_cacheviz.html) +* [[Module dev_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_ans104.erl)](./resources/source-code/dev_ans104.html) +* [[Module dev_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_flat.erl)](./resources/source-code/dev_flat.html) +* [[Module dev_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig.erl)](./resources/source-code/dev_httpsig.html) +* [[Module dev_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig_conv.erl)](./resources/source-code/dev_httpsig_conv.html) +* [[Module dev_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json.erl)](./resources/source-code/dev_json.html) +* [[Module dev_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_structured.erl)](./resources/source-code/dev_structured.html) +* [[Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cron.erl)](./resources/source-code/dev_cron.html) +* [[Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_dedup.erl)](./resources/source-code/dev_dedup.html) +* [[Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_delegated_compute.erl)](./resources/source-code/dev_delegated_compute.html) +* [[Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_faff.erl)](./resources/source-code/dev_faff.html) +* [[Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_genesis_wasm.erl)](./resources/source-code/dev_genesis_wasm.html) +* [[Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_green_zone.erl)](./resources/source-code/dev_green_zone.html) +* [[Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hook.erl)](./resources/source-code/dev_hook.html) +* [[Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hyperbuddy.erl)](./resources/source-code/dev_hyperbuddy.html) +* [[Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json_iface.erl)](./resources/source-code/dev_json_iface.html) +* [[Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_local_name.erl)](./resources/source-code/dev_local_name.html) +* [[Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua.erl)](./resources/source-code/dev_lua.html) +* [[Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_lib.erl)](./resources/source-code/dev_lua_lib.html) +* [[Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_test.erl)](./resources/source-code/dev_lua_test.html) +* [[Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_manifest.erl)](./resources/source-code/dev_manifest.html) +* [[Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_message.erl)](./resources/source-code/dev_message.html) +* [[Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_meta.erl)](./resources/source-code/dev_meta.html) +* [[Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_multipass.erl)](./resources/source-code/dev_multipass.html) +* [[Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_name.erl)](./resources/source-code/dev_name.html) +* [[Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_node_process.erl)](./resources/source-code/dev_node_process.html) +* [[Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_p4.erl)](./resources/source-code/dev_p4.html) +* [[Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_patch.erl)](./resources/source-code/dev_patch.html) +* [[Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_poda.erl)](./resources/source-code/dev_poda.html) +* [[Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process.erl)](./resources/source-code/dev_process.html) +* [[Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_cache.erl)](./resources/source-code/dev_process_cache.html) +* [[Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_worker.erl)](./resources/source-code/dev_process_worker.html) +* [[Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_push.erl)](./resources/source-code/dev_push.html) +* [[Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_relay.erl)](./resources/source-code/dev_relay.html) +* [[Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_router.erl)](./resources/source-code/dev_router.html) +* [[Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler.erl)](./resources/source-code/dev_scheduler.html) +* [[Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_cache.erl)](./resources/source-code/dev_scheduler_cache.html) +* [[Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_formats.erl)](./resources/source-code/dev_scheduler_formats.html) +* [[Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_registry.erl)](./resources/source-code/dev_scheduler_registry.html) +* [[Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_server.erl)](./resources/source-code/dev_scheduler_server.html) +* [[Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_simple_pay.erl)](./resources/source-code/dev_simple_pay.html) +* [[Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_snp.erl)](./resources/source-code/dev_snp.html) +* [[Module hb_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/core/hb_snp_nif.erl)](./resources/source-code/hb_snp_nif.html) +* [[Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_stack.erl)](./resources/source-code/dev_stack.html) +* [[Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_test.erl)](./resources/source-code/dev_test.html) +* [[Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasi.erl)](./resources/source-code/dev_wasi.html) +* [[Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasm.erl)](./resources/source-code/dev_wasm.html) * [[Module hb.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb.erl)](./resources/source-code/hb.html) * [[Module hb_ao.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_ao.erl)](./resources/source-code/hb_ao.html) * [[Module hb_ao_test_vectors.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_ao_test_vectors.erl)](./resources/source-code/hb_ao_test_vectors.html) @@ -149,7 +145,6 @@ Key sections include: Getting Started (begin), Running HyperBEAM (run), Develope * [[Module hb_test_utils.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_test_utils.erl)](./resources/source-code/hb_test_utils.html) * [[Module hb_tracer.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_tracer.erl)](./resources/source-code/hb_tracer.html) * [[Module hb_util.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_util.erl)](./resources/source-code/hb_util.html) -* [[Module hb_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_volume.erl)](./resources/source-code/hb_volume.html) * [Source Code Documentation](./resources/source-code/index.html) * [The hb application #](./resources/source-code/README.html) * [[Module rsa_pss.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/rsa_pss.erl)](./resources/source-code/rsa_pss.html) diff --git a/docs/misc/hacking-on-hyperbeam.md b/docs/misc/hacking-on-hyperbeam.md index 6df0f5a9d..ebb69cbcd 100644 --- a/docs/misc/hacking-on-hyperbeam.md +++ b/docs/misc/hacking-on-hyperbeam.md @@ -103,4 +103,29 @@ since the last invocation. 3. Open the svg file in browser. -Happy hacking! \ No newline at end of file +## Common testing pitfalls + +Here is a helpful list of common mistakes when writing tests: + +- If you need to start a new node, be sure to use a new private key unless you + have a specific reason to use an existing one. HyperBEAM HTTP servers are + registered using their wallet ID as their 'name', so re-use can cause issues. + You can get a new private key is defined using `#{ priv_wallet => ar_wallet:new() }`. +- Similarly, always be careful of your stores in your tests! Avoid using the + default stores, as this can lead to 'context leakage', where one part of your + test is unintentionally able to access data created/stored by a supposedly + different node in the environment. `hb_http_server:start_node/1` will generate + a new unique store for you by default, but avoid creating a named store unless + you need to (and know what you are doing). +- Always try to test your devices through the HTTP AO-Core API as well as through + the local `hb_ao:resolve/[2-3]` interfaces. Avoid direct `dev_name:key` calls + unless strictly necessary. The HTTP API is how users will interact with your + almost always system, and there can be subtle differences in how the interfaces + react. For example, the Erlang function call interface has no regard for how + keys are matched by AO-Core, so will mask any issues with the choice of which + device function to call to satisfy requests. + +Happy hacking! + +Avoid pattern match a list of commitments, since we cannot guarantee the order. +This will case tests to be flaky. diff --git a/docs/misc/installation-core/hyperbeam-setup-config/configuration/configuration-options.md b/docs/misc/installation-core/hyperbeam-setup-config/configuration/configuration-options.md index 4ac2100c2..1a07d989f 100644 --- a/docs/misc/installation-core/hyperbeam-setup-config/configuration/configuration-options.md +++ b/docs/misc/installation-core/hyperbeam-setup-config/configuration/configuration-options.md @@ -78,7 +78,9 @@ These options control how HyperBEAM manages devices. | Option | Type | Default | Description | |--------|------|---------|-------------| -| `preloaded_devices` | Map | (see code) | Devices for the node to use, overriding resolution via ID | +| `preloaded_store` | Store map | `{store-module: hb_store_lmdb, name: "_build/preloaded-store"}` | LMDB store of signed device specs and impl messages baked at build time. | +| `preloaded_devices_index` | Binary | (filled by build) | Committed ID of the preloaded-store resolver message. | +| `device_store` | Store map | falls back to `store` | Volatile cache of name/spec-ID → loaded module atom. | | `load_remote_devices` | Boolean | false | Whether to load devices from remote signers | | `devices` | List | [] | Additional devices to load | @@ -110,4 +112,4 @@ These options control debugging and development features. ## Complete Option List -For the most up-to-date list of configuration options, refer to the `default_message/0` function in the `hb_opts` module in the HyperBEAM source code. \ No newline at end of file +For the most up-to-date list of configuration options, refer to the `default_message/0` function in the `hb_opts` module in the HyperBEAM source code. diff --git a/docs/misc/installation-core/hyperbeam-setup-config/index.md b/docs/misc/installation-core/hyperbeam-setup-config/index.md index a698f0d54..3d9182b7f 100644 --- a/docs/misc/installation-core/hyperbeam-setup-config/index.md +++ b/docs/misc/installation-core/hyperbeam-setup-config/index.md @@ -24,7 +24,7 @@ Key properties of messages: ## Devices -HyperBEAM supports numerous devices, each enabling different services. There are approximately 25 different devices included in the preloaded_devices of a HyperBEAM node. +HyperBEAM supports numerous devices, each enabling different services. There are approximately 25 different devices baked into a HyperBEAM node's preloaded-store. ### Key Preloaded Devices diff --git a/docs/misc/installation-core/system-dependencies/installation/index.md b/docs/misc/installation-core/system-dependencies/installation/index.md index 6a10950a1..6ff794652 100644 --- a/docs/misc/installation-core/system-dependencies/installation/index.md +++ b/docs/misc/installation-core/system-dependencies/installation/index.md @@ -10,7 +10,7 @@ For the best experience, we recommend installing prerequisites in this order: 2. Erlang/OTP (programming language for HyperBEAM) 3. Rebar3 (Erlang build tool) 4. Node.js (required for the Compute Unit) -5. Rust (required for the dev_snp_nif) +5. Rust (required for the hb_snp_nif) ## Component Guides @@ -20,7 +20,7 @@ Follow these guides in sequence to set up your environment: 2. [Erlang Installation](erlang.md) - Programming language for HyperBEAM 3. [Rebar3 Installation](rebar3.md) - Build tool for Erlang 4. [Node.js Installation](nodejs.md) - Required for the Compute Unit -5. [Rust Installation](rust.md) - Required for the dev_snp_nif +5. [Rust Installation](rust.md) - Required for the hb_snp_nif ## Next Steps diff --git a/docs/misc/setting-up-selecting-devices.md b/docs/misc/setting-up-selecting-devices.md index 9268ad0c3..3082bf501 100644 --- a/docs/misc/setting-up-selecting-devices.md +++ b/docs/misc/setting-up-selecting-devices.md @@ -56,7 +56,7 @@ If you want to monetize your node's services: If security is a priority: - **~snp@1.0**: For generating and validating proofs that a node is executing inside a Trusted Execution Environment (TEE) -- **dev_codec_httpsig**: Implements HTTP Message Signatures as described in RFC-9421 +- **dev_httpsig**: Implements HTTP Message Signatures as described in RFC-9421 ### Legacynet Compatibility @@ -154,7 +154,7 @@ A "friends and family" pricing policy that allows users to process requests only Generates and validates proofs that a node is executing inside a Trusted Execution Environment (TEE). Nodes executing inside TEEs use an ephemeral key pair that provably exists only inside the TEE. -### dev_codec_httpsig +### dev_httpsig Implements HTTP Message Signatures as described in RFC-9421, providing a way to authenticate and verify the integrity of HTTP messages. @@ -168,10 +168,6 @@ Inserts new messages into the schedule to allow processes to passively 'call' th Deduplicates messages sent to a process. It runs on the first pass of a `compute` key call if executed in a stack, preventing duplicate processing. -### dev_monitor - -Allows flexible monitoring of a process execution. Adding this device to a process will call specified functions with the current process state during each pass. - ### dev_multipass Triggers repass events until a certain counter has been reached. Useful for stacks that need various execution passes to be completed in sequence across devices. @@ -258,7 +254,7 @@ rebar3 shell --eval "hb:start_mainnet(#{ p4_pricing-device => '~simple-pay@1.0', p4_ledger-device => '~simple-pay@1.0', simple_pay_price => 0.01, - preloaded_devices => ['~wasm64@1.0', '~process@1.0', 'dev_stack', 'dev_scheduler'] + admissible-devices => ['~wasm64@1.0', '~process@1.0', 'dev_stack', 'dev_scheduler'] })." ``` @@ -275,7 +271,7 @@ rebar3 shell --eval "hb:start_mainnet(#{ p4_pricing-device => '~simple-pay@1.0', p4_ledger-device => '~simple-pay@1.0', simple_pay_price => 0.05, - preloaded_devices => ['~wasm64@1.0', '~process@1.0', 'dev_stack', 'dev_scheduler', '~snp@1.0'] + admissible-devices => ['~wasm64@1.0', '~process@1.0', 'dev_stack', 'dev_scheduler', '~snp@1.0'] })." ``` diff --git a/docs/resources/source-code/README.md b/docs/resources/source-code/README.md index 7e15fb008..a8f983b88 100644 --- a/docs/resources/source-code/README.md +++ b/docs/resources/source-code/README.md @@ -15,14 +15,13 @@ ar_wallet dev_cache dev_cacheviz -dev_codec_ans104 -dev_codec_flat -dev_codec_httpsig -dev_codec_httpsig_conv -dev_codec_json -dev_codec_structured +dev_ans104 +dev_flat +dev_httpsig +dev_httpsig_conv +dev_json +dev_structured dev_cron -dev_cu dev_dedup dev_delegated_compute dev_faff @@ -32,14 +31,12 @@ dev_hyperbuddy dev_json_iface dev_local_name -dev_lookup dev_lua dev_lua_lib dev_lua_test dev_manifest dev_message dev_meta -dev_monitor dev_multipass dev_name dev_node_process @@ -59,10 +56,9 @@ dev_scheduler_server dev_simple_pay dev_snp -dev_snp_nif +hb_snp_nif dev_stack dev_test -dev_volume dev_wasi dev_wasm hb @@ -110,6 +106,4 @@ hb_test_utils hb_tracer hb_util -hb_volume rsa_pss - diff --git a/docs/resources/source-code/dev_codec_ans104.md b/docs/resources/source-code/dev_ans104.md similarity index 98% rename from docs/resources/source-code/dev_codec_ans104.md rename to docs/resources/source-code/dev_ans104.md index a597b00be..d52117c19 100644 --- a/docs/resources/source-code/dev_codec_ans104.md +++ b/docs/resources/source-code/dev_ans104.md @@ -1,4 +1,4 @@ -# [Module dev_codec_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_ans104.erl) +# [Module dev_ans104.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_ans104.erl) diff --git a/docs/resources/source-code/dev_cache.md b/docs/resources/source-code/dev_cache.md index f94697515..a199b82bf 100644 --- a/docs/resources/source-code/dev_cache.md +++ b/docs/resources/source-code/dev_cache.md @@ -1,4 +1,4 @@ -# [Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cache.erl) +# [Module dev_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cache.erl) diff --git a/docs/resources/source-code/dev_cacheviz.md b/docs/resources/source-code/dev_cacheviz.md index 60dd68313..4eb7c11bf 100644 --- a/docs/resources/source-code/dev_cacheviz.md +++ b/docs/resources/source-code/dev_cacheviz.md @@ -1,4 +1,4 @@ -# [Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cacheviz.erl) +# [Module dev_cacheviz.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cacheviz.erl) diff --git a/docs/resources/source-code/dev_cron.md b/docs/resources/source-code/dev_cron.md index 264bf9c5a..bd0b25520 100644 --- a/docs/resources/source-code/dev_cron.md +++ b/docs/resources/source-code/dev_cron.md @@ -1,4 +1,4 @@ -# [Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cron.erl) +# [Module dev_cron.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_cron.erl) diff --git a/docs/resources/source-code/dev_cu.md b/docs/resources/source-code/dev_cu.md deleted file mode 100644 index 6442e165e..000000000 --- a/docs/resources/source-code/dev_cu.md +++ /dev/null @@ -1,29 +0,0 @@ -# [Module dev_cu.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_cu.erl) - - - - - - -## Function Index ## - - -
execute/2
push/2
- - - - -## Function Details ## - - - -### execute/2 ### - -`execute(CarrierMsg, S) -> any()` - - - -### push/2 ### - -`push(Msg, S) -> any()` - diff --git a/docs/resources/source-code/dev_dedup.md b/docs/resources/source-code/dev_dedup.md index 7578408fe..489ef613a 100644 --- a/docs/resources/source-code/dev_dedup.md +++ b/docs/resources/source-code/dev_dedup.md @@ -1,4 +1,4 @@ -# [Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_dedup.erl) +# [Module dev_dedup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_dedup.erl) diff --git a/docs/resources/source-code/dev_delegated_compute.md b/docs/resources/source-code/dev_delegated_compute.md index ffb32c430..5223de6b5 100644 --- a/docs/resources/source-code/dev_delegated_compute.md +++ b/docs/resources/source-code/dev_delegated_compute.md @@ -1,4 +1,4 @@ -# [Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_delegated_compute.erl) +# [Module dev_delegated_compute.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_delegated_compute.erl) diff --git a/docs/resources/source-code/dev_faff.md b/docs/resources/source-code/dev_faff.md index 90020b84a..736b69f37 100644 --- a/docs/resources/source-code/dev_faff.md +++ b/docs/resources/source-code/dev_faff.md @@ -1,4 +1,4 @@ -# [Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_faff.erl) +# [Module dev_faff.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_faff.erl) diff --git a/docs/resources/source-code/dev_codec_flat.md b/docs/resources/source-code/dev_flat.md similarity index 96% rename from docs/resources/source-code/dev_codec_flat.md rename to docs/resources/source-code/dev_flat.md index 912442777..dc53bcc04 100644 --- a/docs/resources/source-code/dev_codec_flat.md +++ b/docs/resources/source-code/dev_flat.md @@ -1,4 +1,4 @@ -# [Module dev_codec_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_flat.erl) +# [Module dev_flat.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_flat.erl) diff --git a/docs/resources/source-code/dev_genesis_wasm.md b/docs/resources/source-code/dev_genesis_wasm.md index d4839976b..81eefb265 100644 --- a/docs/resources/source-code/dev_genesis_wasm.md +++ b/docs/resources/source-code/dev_genesis_wasm.md @@ -1,4 +1,4 @@ -# [Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_genesis_wasm.erl) +# [Module dev_genesis_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_genesis_wasm.erl) diff --git a/docs/resources/source-code/dev_green_zone.md b/docs/resources/source-code/dev_green_zone.md index 6ff9b42e7..ee75af969 100644 --- a/docs/resources/source-code/dev_green_zone.md +++ b/docs/resources/source-code/dev_green_zone.md @@ -1,4 +1,4 @@ -# [Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_green_zone.erl) +# [Module dev_green_zone.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_green_zone.erl) @@ -17,7 +17,7 @@ commitment and encryption.
add_trusted_node/4*Adds a node to the trusted nodes list with its commitment report.
become/3Clones the identity of a target node in the green zone.
calculate_node_message/3*Generate the node message that should be set prior to joining -a green zone.
decrypt_zone_key/2*Decrypts an AES key using the node's RSA private key.
default_zone_required_opts/1*Provides the default required options for a green zone.
encrypt_payload/2*Encrypts an AES key with a node's RSA public key.
finalize_become/5*
info/1Controls which functions are exposed via the device API.
info/3Provides information about the green zone device and its API.
init/3Initialize the green zone for a node.
join/3Initiates the join process for a node to enter an existing green zone.
join_peer/5*Processes a join request to a specific peer node.
key/3Encrypts and provides the node's private key for secure sharing.
maybe_set_zone_opts/4*Adopts configuration from a peer when joining a green zone.
rsa_wallet_integration_test/0*Test RSA operations with the existing wallet structure.
try_mount_encrypted_volume/2*Attempts to mount an encrypted volume using the green zone AES key.
validate_join/3*Validates an incoming join request from another node.
validate_peer_opts/2*Validates that a peer's configuration matches required options.
+a green zone.decrypt_zone_key/2*Decrypts an AES key using the node's RSA private key.default_zone_required_opts/1*Provides the default required options for a green zone.encrypt_payload/2*Encrypts an AES key with a node's RSA public key.finalize_become/5*info/1Controls which functions are exposed via the device API.info/3Provides information about the green zone device and its API.init/3Initialize the green zone for a node.join/3Initiates the join process for a node to enter an existing green zone.join_peer/5*Processes a join request to a specific peer node.key/3Encrypts and provides the node's private key for secure sharing.maybe_set_zone_opts/4*Adopts configuration from a peer when joining a green zone.rsa_wallet_integration_test/0*Test RSA operations with the existing wallet structure.validate_join/3*Validates an incoming join request from another node.validate_peer_opts/2*Validates that a peer's configuration matches required options. @@ -252,7 +252,7 @@ join_peer(PeerLocation::binary(), PeerID::binary(), M1::term(), M2::term(), Opts
-`PeerLocation`: The target peer's address
`PeerID`: The target peer's unique identifier
`M2`: May contain ShouldMount flag to enable encrypted volume mounting
+`PeerLocation`: The target peer's address
`PeerID`: The target peer's unique identifier
`M2`: Additional request details
returns: `{ok, Map}` on success with confirmation message, or `{error, Map|Binary}` on failure with error details @@ -267,7 +267,6 @@ This function handles the client-side join flow when connecting to a peer: 5. Verifies the response signature 6. Decrypts the returned AES key 7. Updates local configuration with the shared key -8. Optionally mounts an encrypted volume using the shared key @@ -335,21 +334,6 @@ from the wallet work correctly. It creates a new wallet, encrypts a test message with the RSA public key, and then decrypts it with the RSA private key, asserting that the decrypted message matches the original. - - -### try_mount_encrypted_volume/2 * ### - -`try_mount_encrypted_volume(AESKey, Opts) -> any()` - -Attempts to mount an encrypted volume using the green zone AES key. - -This function handles the complete process of secure storage setup by -delegating to the dev_volume module, which provides a unified interface -for volume management. - -The encryption key used for the volume is the same AES key used for green zone -communication, ensuring that only nodes in the green zone can access the data. - ### validate_join/3 * ### diff --git a/docs/resources/source-code/dev_hook.md b/docs/resources/source-code/dev_hook.md index 477df1e5f..c867f3d53 100644 --- a/docs/resources/source-code/dev_hook.md +++ b/docs/resources/source-code/dev_hook.md @@ -1,4 +1,4 @@ -# [Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hook.erl) +# [Module dev_hook.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hook.erl) diff --git a/docs/resources/source-code/dev_codec_httpsig.md b/docs/resources/source-code/dev_httpsig.md similarity index 99% rename from docs/resources/source-code/dev_codec_httpsig.md rename to docs/resources/source-code/dev_httpsig.md index 974533e6a..d0a40fc38 100644 --- a/docs/resources/source-code/dev_codec_httpsig.md +++ b/docs/resources/source-code/dev_httpsig.md @@ -1,4 +1,4 @@ -# [Module dev_codec_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig.erl) +# [Module dev_httpsig.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig.erl) @@ -12,7 +12,7 @@ This module implements HTTP Message Signatures as described in RFC-9421 It implements the codec standard (from/1, to/1), as well as the optional commitment functions (id/3, sign/3, verify/3). The commitment functions are found in this module, while the codec functions are relayed to the -`dev_codec_httpsig_conv` module. +`dev_httpsig_conv` module. ## Data Types ## diff --git a/docs/resources/source-code/dev_codec_httpsig_conv.md b/docs/resources/source-code/dev_httpsig_conv.md similarity index 98% rename from docs/resources/source-code/dev_codec_httpsig_conv.md rename to docs/resources/source-code/dev_httpsig_conv.md index 17f5f28ca..681ee44e7 100644 --- a/docs/resources/source-code/dev_codec_httpsig_conv.md +++ b/docs/resources/source-code/dev_httpsig_conv.md @@ -1,4 +1,4 @@ -# [Module dev_codec_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_httpsig_conv.erl) +# [Module dev_httpsig_conv.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_httpsig_conv.erl) diff --git a/docs/resources/source-code/dev_hyperbuddy.md b/docs/resources/source-code/dev_hyperbuddy.md index da40fdaaf..9a3adbdfb 100644 --- a/docs/resources/source-code/dev_hyperbuddy.md +++ b/docs/resources/source-code/dev_hyperbuddy.md @@ -1,4 +1,4 @@ -# [Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_hyperbuddy.erl) +# [Module dev_hyperbuddy.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_hyperbuddy.erl) diff --git a/docs/resources/source-code/dev_codec_json.md b/docs/resources/source-code/dev_json.md similarity index 95% rename from docs/resources/source-code/dev_codec_json.md rename to docs/resources/source-code/dev_json.md index 9f69c1b45..35fe93c79 100644 --- a/docs/resources/source-code/dev_codec_json.md +++ b/docs/resources/source-code/dev_json.md @@ -1,4 +1,4 @@ -# [Module dev_codec_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_json.erl) +# [Module dev_json.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json.erl) diff --git a/docs/resources/source-code/dev_json_iface.md b/docs/resources/source-code/dev_json_iface.md index dca4dd7b4..c5422befc 100644 --- a/docs/resources/source-code/dev_json_iface.md +++ b/docs/resources/source-code/dev_json_iface.md @@ -1,4 +1,4 @@ -# [Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_json_iface.erl) +# [Module dev_json_iface.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_json_iface.erl) diff --git a/docs/resources/source-code/dev_local_name.md b/docs/resources/source-code/dev_local_name.md index c536e6cec..28253530d 100644 --- a/docs/resources/source-code/dev_local_name.md +++ b/docs/resources/source-code/dev_local_name.md @@ -1,4 +1,4 @@ -# [Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_local_name.erl) +# [Module dev_local_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_local_name.erl) diff --git a/docs/resources/source-code/dev_lookup.md b/docs/resources/source-code/dev_lookup.md deleted file mode 100644 index 6ed34a647..000000000 --- a/docs/resources/source-code/dev_lookup.md +++ /dev/null @@ -1,52 +0,0 @@ -# [Module dev_lookup.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lookup.erl) - - - - -A device that looks up an ID from a local store and returns it, honoring -the `accept` key to return the correct format. - - - -## Function Index ## - - -
aos2_message_lookup_test/0*
binary_lookup_test/0*
http_lookup_test/0*
message_lookup_test/0*
read/3Fetch a resource from the cache using "target" ID extracted from the message.
- - - - -## Function Details ## - - - -### aos2_message_lookup_test/0 * ### - -`aos2_message_lookup_test() -> any()` - - - -### binary_lookup_test/0 * ### - -`binary_lookup_test() -> any()` - - - -### http_lookup_test/0 * ### - -`http_lookup_test() -> any()` - - - -### message_lookup_test/0 * ### - -`message_lookup_test() -> any()` - - - -### read/3 ### - -`read(M1, M2, Opts) -> any()` - -Fetch a resource from the cache using "target" ID extracted from the message - diff --git a/docs/resources/source-code/dev_lua.md b/docs/resources/source-code/dev_lua.md index 9101078aa..f13447a3a 100644 --- a/docs/resources/source-code/dev_lua.md +++ b/docs/resources/source-code/dev_lua.md @@ -1,4 +1,4 @@ -# [Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua.erl) +# [Module dev_lua.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua.erl) diff --git a/docs/resources/source-code/dev_lua_lib.md b/docs/resources/source-code/dev_lua_lib.md index 016e90a0b..c95e19a56 100644 --- a/docs/resources/source-code/dev_lua_lib.md +++ b/docs/resources/source-code/dev_lua_lib.md @@ -1,4 +1,4 @@ -# [Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_lib.erl) +# [Module dev_lua_lib.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_lib.erl) diff --git a/docs/resources/source-code/dev_lua_test.md b/docs/resources/source-code/dev_lua_test.md index 7aed2bb75..dda7b8366 100644 --- a/docs/resources/source-code/dev_lua_test.md +++ b/docs/resources/source-code/dev_lua_test.md @@ -1,4 +1,4 @@ -# [Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_lua_test.erl) +# [Module dev_lua_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_lua_test.erl) diff --git a/docs/resources/source-code/dev_manifest.md b/docs/resources/source-code/dev_manifest.md index 4ee517450..4bc22a4f4 100644 --- a/docs/resources/source-code/dev_manifest.md +++ b/docs/resources/source-code/dev_manifest.md @@ -1,4 +1,4 @@ -# [Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_manifest.erl) +# [Module dev_manifest.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_manifest.erl) diff --git a/docs/resources/source-code/dev_message.md b/docs/resources/source-code/dev_message.md index 714013bc9..832e2c179 100644 --- a/docs/resources/source-code/dev_message.md +++ b/docs/resources/source-code/dev_message.md @@ -1,4 +1,4 @@ -# [Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_message.erl) +# [Module dev_message.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_message.erl) diff --git a/docs/resources/source-code/dev_meta.md b/docs/resources/source-code/dev_meta.md index a7f2d5d51..1c83d10e8 100644 --- a/docs/resources/source-code/dev_meta.md +++ b/docs/resources/source-code/dev_meta.md @@ -1,4 +1,4 @@ -# [Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_meta.erl) +# [Module dev_meta.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_meta.erl) diff --git a/docs/resources/source-code/dev_monitor.md b/docs/resources/source-code/dev_monitor.md deleted file mode 100644 index f092fb0a0..000000000 --- a/docs/resources/source-code/dev_monitor.md +++ /dev/null @@ -1,53 +0,0 @@ -# [Module dev_monitor.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_monitor.erl) - - - - - - -## Function Index ## - - -
add_monitor/2
end_of_schedule/1
execute/2
init/3
signal/2*
uses/0
- - - - -## Function Details ## - - - -### add_monitor/2 ### - -`add_monitor(Mon, State) -> any()` - - - -### end_of_schedule/1 ### - -`end_of_schedule(State) -> any()` - - - -### execute/2 ### - -`execute(Message, State) -> any()` - - - -### init/3 ### - -`init(State, X2, InitState) -> any()` - - - -### signal/2 * ### - -`signal(State, Signal) -> any()` - - - -### uses/0 ### - -`uses() -> any()` - diff --git a/docs/resources/source-code/dev_multipass.md b/docs/resources/source-code/dev_multipass.md index 1cf3fec8a..7763c5432 100644 --- a/docs/resources/source-code/dev_multipass.md +++ b/docs/resources/source-code/dev_multipass.md @@ -1,4 +1,4 @@ -# [Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_multipass.erl) +# [Module dev_multipass.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_multipass.erl) diff --git a/docs/resources/source-code/dev_name.md b/docs/resources/source-code/dev_name.md index 9edd22fca..58ebf93d1 100644 --- a/docs/resources/source-code/dev_name.md +++ b/docs/resources/source-code/dev_name.md @@ -1,4 +1,4 @@ -# [Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_name.erl) +# [Module dev_name.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_name.erl) diff --git a/docs/resources/source-code/dev_node_process.md b/docs/resources/source-code/dev_node_process.md index 70a546cd9..6cb672e81 100644 --- a/docs/resources/source-code/dev_node_process.md +++ b/docs/resources/source-code/dev_node_process.md @@ -1,4 +1,4 @@ -# [Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_node_process.erl) +# [Module dev_node_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_node_process.erl) diff --git a/docs/resources/source-code/dev_p4.md b/docs/resources/source-code/dev_p4.md index 081a4814a..85aebd82b 100644 --- a/docs/resources/source-code/dev_p4.md +++ b/docs/resources/source-code/dev_p4.md @@ -1,4 +1,4 @@ -# [Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_p4.erl) +# [Module dev_p4.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_p4.erl) diff --git a/docs/resources/source-code/dev_patch.md b/docs/resources/source-code/dev_patch.md index 92cf5373c..890b32300 100644 --- a/docs/resources/source-code/dev_patch.md +++ b/docs/resources/source-code/dev_patch.md @@ -1,4 +1,4 @@ -# [Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_patch.erl) +# [Module dev_patch.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_patch.erl) diff --git a/docs/resources/source-code/dev_poda.md b/docs/resources/source-code/dev_poda.md index 91427a8ea..e62f344eb 100644 --- a/docs/resources/source-code/dev_poda.md +++ b/docs/resources/source-code/dev_poda.md @@ -1,4 +1,4 @@ -# [Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_poda.erl) +# [Module dev_poda.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_poda.erl) diff --git a/docs/resources/source-code/dev_process.md b/docs/resources/source-code/dev_process.md index f9a4074ea..508503b56 100644 --- a/docs/resources/source-code/dev_process.md +++ b/docs/resources/source-code/dev_process.md @@ -1,4 +1,4 @@ -# [Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process.erl) +# [Module dev_process.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process.erl) diff --git a/docs/resources/source-code/dev_process_cache.md b/docs/resources/source-code/dev_process_cache.md index 59f195d20..16e65f59d 100644 --- a/docs/resources/source-code/dev_process_cache.md +++ b/docs/resources/source-code/dev_process_cache.md @@ -1,4 +1,4 @@ -# [Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_cache.erl) +# [Module dev_process_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_cache.erl) diff --git a/docs/resources/source-code/dev_process_worker.md b/docs/resources/source-code/dev_process_worker.md index 4e34029e3..8f7231031 100644 --- a/docs/resources/source-code/dev_process_worker.md +++ b/docs/resources/source-code/dev_process_worker.md @@ -1,4 +1,4 @@ -# [Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_process_worker.erl) +# [Module dev_process_worker.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_process_worker.erl) diff --git a/docs/resources/source-code/dev_push.md b/docs/resources/source-code/dev_push.md index f966c2457..31805f866 100644 --- a/docs/resources/source-code/dev_push.md +++ b/docs/resources/source-code/dev_push.md @@ -1,4 +1,4 @@ -# [Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_push.erl) +# [Module dev_push.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_push.erl) diff --git a/docs/resources/source-code/dev_relay.md b/docs/resources/source-code/dev_relay.md index 2828b72eb..bc43820ec 100644 --- a/docs/resources/source-code/dev_relay.md +++ b/docs/resources/source-code/dev_relay.md @@ -1,4 +1,4 @@ -# [Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_relay.erl) +# [Module dev_relay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_relay.erl) diff --git a/docs/resources/source-code/dev_router.md b/docs/resources/source-code/dev_router.md index c513e1b3d..82fb7ee9a 100644 --- a/docs/resources/source-code/dev_router.md +++ b/docs/resources/source-code/dev_router.md @@ -1,4 +1,4 @@ -# [Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_router.erl) +# [Module dev_router.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_router.erl) diff --git a/docs/resources/source-code/dev_scheduler.md b/docs/resources/source-code/dev_scheduler.md index e9a1cb10d..67ee48f0e 100644 --- a/docs/resources/source-code/dev_scheduler.md +++ b/docs/resources/source-code/dev_scheduler.md @@ -1,4 +1,4 @@ -# [Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler.erl) +# [Module dev_scheduler.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler.erl) diff --git a/docs/resources/source-code/dev_scheduler_cache.md b/docs/resources/source-code/dev_scheduler_cache.md index 2ddd9866a..7fa3939fd 100644 --- a/docs/resources/source-code/dev_scheduler_cache.md +++ b/docs/resources/source-code/dev_scheduler_cache.md @@ -1,4 +1,4 @@ -# [Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_cache.erl) +# [Module dev_scheduler_cache.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_cache.erl) diff --git a/docs/resources/source-code/dev_scheduler_formats.md b/docs/resources/source-code/dev_scheduler_formats.md index 4c356b752..0476cf2aa 100644 --- a/docs/resources/source-code/dev_scheduler_formats.md +++ b/docs/resources/source-code/dev_scheduler_formats.md @@ -1,4 +1,4 @@ -# [Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_formats.erl) +# [Module dev_scheduler_formats.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_formats.erl) diff --git a/docs/resources/source-code/dev_scheduler_registry.md b/docs/resources/source-code/dev_scheduler_registry.md index 4415c8ff7..541b76e9e 100644 --- a/docs/resources/source-code/dev_scheduler_registry.md +++ b/docs/resources/source-code/dev_scheduler_registry.md @@ -1,4 +1,4 @@ -# [Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_registry.erl) +# [Module dev_scheduler_registry.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_registry.erl) diff --git a/docs/resources/source-code/dev_scheduler_server.md b/docs/resources/source-code/dev_scheduler_server.md index ab8f015fd..66d8defc7 100644 --- a/docs/resources/source-code/dev_scheduler_server.md +++ b/docs/resources/source-code/dev_scheduler_server.md @@ -1,4 +1,4 @@ -# [Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_scheduler_server.erl) +# [Module dev_scheduler_server.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_scheduler_server.erl) diff --git a/docs/resources/source-code/dev_simple_pay.md b/docs/resources/source-code/dev_simple_pay.md index b65230680..f28e0dff1 100644 --- a/docs/resources/source-code/dev_simple_pay.md +++ b/docs/resources/source-code/dev_simple_pay.md @@ -1,4 +1,4 @@ -# [Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_simple_pay.erl) +# [Module dev_simple_pay.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_simple_pay.erl) diff --git a/docs/resources/source-code/dev_snp.md b/docs/resources/source-code/dev_snp.md index 9eb969850..31567bb81 100644 --- a/docs/resources/source-code/dev_snp.md +++ b/docs/resources/source-code/dev_snp.md @@ -1,4 +1,4 @@ -# [Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp.erl) +# [Module dev_snp.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_snp.erl) diff --git a/docs/resources/source-code/dev_stack.md b/docs/resources/source-code/dev_stack.md index d00bc24cf..8016a1f72 100644 --- a/docs/resources/source-code/dev_stack.md +++ b/docs/resources/source-code/dev_stack.md @@ -1,4 +1,4 @@ -# [Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_stack.erl) +# [Module dev_stack.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_stack.erl) diff --git a/docs/resources/source-code/dev_codec_structured.md b/docs/resources/source-code/dev_structured.md similarity index 96% rename from docs/resources/source-code/dev_codec_structured.md rename to docs/resources/source-code/dev_structured.md index 1b28f9735..cc81e8349 100644 --- a/docs/resources/source-code/dev_codec_structured.md +++ b/docs/resources/source-code/dev_structured.md @@ -1,4 +1,4 @@ -# [Module dev_codec_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_codec_structured.erl) +# [Module dev_structured.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_structured.erl) diff --git a/docs/resources/source-code/dev_test.md b/docs/resources/source-code/dev_test.md index 854ac1d7d..21a22e554 100644 --- a/docs/resources/source-code/dev_test.md +++ b/docs/resources/source-code/dev_test.md @@ -1,4 +1,4 @@ -# [Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_test.erl) +# [Module dev_test.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_test.erl) diff --git a/docs/resources/source-code/dev_volume.md b/docs/resources/source-code/dev_volume.md deleted file mode 100644 index 210df23d9..000000000 --- a/docs/resources/source-code/dev_volume.md +++ /dev/null @@ -1,271 +0,0 @@ -# [Module dev_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_volume.erl) - - - - -Secure Volume Management for HyperBEAM Nodes. - - - -## Description ## - -This module handles encrypted storage operations for HyperBEAM, providing -a robust and secure approach to data persistence. It manages the complete -lifecycle of encrypted volumes from detection to creation, formatting, and -mounting. - -Key responsibilities: -- Volume detection and initialization -- Encrypted partition creation and formatting -- Secure mounting using cryptographic keys -- Store path reconfiguration to use mounted volumes -- Automatic handling of various system states -(new device, existing partition, etc.) - -The primary entry point is the `mount/3` function, which orchestrates the -entire process based on the provided configuration parameters. This module -works alongside `hb_volume` which provides the low-level operations for -device manipulation. - -Security considerations: -- Ensures data at rest is protected through LUKS encryption -- Provides proper volume sanitization and secure mounting -- IMPORTANT: This module only applies configuration set in node options and -does NOT accept disk operations via HTTP requests. It cannot format arbitrary -disks as all operations are safeguarded by host operating system permissions -enforced upon the HyperBEAM environment. - -## Function Index ## - - -
check_base_device/8*Check if the base device exists and if it does, check if the partition exists.
check_partition/8*Check if the partition exists.
create_and_mount_partition/8*Create, format and mount a new partition.
decrypt_volume_key/2*Decrypts an encrypted volume key using the node's private key.
format_and_mount/6*Format and mount a newly created partition.
info/1Exported function for getting device info, controls which functions are -exposed via the device API.
info/3HTTP info response providing information about this device.
mount/3Handles the complete process of secure encrypted volume mounting.
mount_existing_partition/6*Mount an existing partition.
mount_formatted_partition/6*Mount a newly formatted partition.
public_key/3Returns the node's public key for secure key exchange.
update_node_config/2*Update the node's configuration with the new store.
update_store_path/2*Update the store path to use the mounted volume.
- - - - -## Function Details ## - - - -### check_base_device/8 * ### - -

-check_base_device(Device::term(), Partition::term(), PartitionType::term(), VolumeName::term(), MountPoint::term(), StorePath::term(), Key::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The base device to check.
`Partition`: The partition to check.
`PartitionType`: The type of partition to check.
`VolumeName`: The name of the volume to check.
`MountPoint`: The mount point to check.
`StorePath`: The store path to check.
`Key`: The key to check.
`Opts`: The options to check.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Check if the base device exists and if it does, check if the partition exists. - - - -### check_partition/8 * ### - -

-check_partition(Device::term(), Partition::term(), PartitionType::term(), VolumeName::term(), MountPoint::term(), StorePath::term(), Key::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The base device to check.
`Partition`: The partition to check.
`PartitionType`: The type of partition to check.
`VolumeName`: The name of the volume to check.
`MountPoint`: The mount point to check.
`StorePath`: The store path to check.
`Key`: The key to check.
`Opts`: The options to check.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Check if the partition exists. If it does, attempt to mount it. -If it doesn't exist, create it, format it with encryption and mount it. - - - -### create_and_mount_partition/8 * ### - -

-create_and_mount_partition(Device::term(), Partition::term(), PartitionType::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Device`: The device to create the partition on.
`Partition`: The partition to create.
`PartitionType`: The type of partition to create.
`Key`: The key to create the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Create, format and mount a new partition. - - - -### decrypt_volume_key/2 * ### - -

-decrypt_volume_key(EncryptedKeyBase64::binary(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Opts`: A map of configuration options.
- -returns: `{ok, DecryptedKey}` on successful decryption, or -`{error, Binary}` if decryption fails. - -Decrypts an encrypted volume key using the node's private key. - -This function takes an encrypted key (typically sent by a client who encrypted -it with the node's public key) and decrypts it using the node's private RSA key. - - - -### format_and_mount/6 * ### - -

-format_and_mount(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to format and mount.
`Key`: The key to format and mount the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Format and mount a newly created partition. - - - -### info/1 ### - -`info(X1) -> any()` - -Exported function for getting device info, controls which functions are -exposed via the device API. - - - -### info/3 ### - -`info(Msg1, Msg2, Opts) -> any()` - -HTTP info response providing information about this device - - - -### mount/3 ### - -

-mount(M1::term(), M2::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`M1`: Base message for context.
`M2`: Request message with operation details.
`Opts`: A map of configuration options for volume operations.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Handles the complete process of secure encrypted volume mounting. - -This function performs the following operations depending on the state: -1. Validates the encryption key is present -2. Checks if the base device exists -3. Checks if the partition exists on the device -4. If the partition exists, attempts to mount it -5. If the partition doesn't exist, creates it, formats it with encryption -and mounts it -6. Updates the node's store configuration to use the mounted volume - -Config options in Opts map: -- volume_key: (Required) The encryption key -- volume_device: Base device path -- volume_partition: Partition path -- volume_partition_type: Filesystem type -- volume_name: Name for encrypted volume -- volume_mount_point: Where to mount -- volume_store_path: Store path on volume - - - -### mount_existing_partition/6 * ### - -

-mount_existing_partition(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to mount.
`Key`: The key to mount.
`MountPoint`: The mount point to mount.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Mount an existing partition. - - - -### mount_formatted_partition/6 * ### - -

-mount_formatted_partition(Partition::term(), Key::term(), MountPoint::term(), VolumeName::term(), StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`Partition`: The partition to mount.
`Key`: The key to mount the partition with.
`MountPoint`: The mount point to mount the partition to.
`VolumeName`: The name of the volume to mount.
`StorePath`: The store path to mount.
`Opts`: The options to mount.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Mount a newly formatted partition. - - - -### public_key/3 ### - -

-public_key(M1::term(), M2::term(), Opts::map()) -> {ok, map()} | {error, binary()}
-
-
- -`Opts`: A map of configuration options.
- -returns: `{ok, Map}` containing the node's public key on success, or -`{error, Binary}` if the node's wallet is not available. - -Returns the node's public key for secure key exchange. - -This function retrieves the node's wallet and extracts the public key -for encryption purposes. It allows users to securely exchange encryption keys -by first encrypting their volume key with the node's public key. - -The process ensures that sensitive keys are never transmitted in plaintext. -The encrypted key can then be securely sent to the node, which will decrypt it -using its private key before using it for volume encryption. - - - -### update_node_config/2 * ### - -

-update_node_config(NewStore::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`NewStore`: The new store to update the node's configuration with.
`Opts`: The options to update the node's configuration with.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Update the node's configuration with the new store. - - - -### update_store_path/2 * ### - -

-update_store_path(StorePath::term(), Opts::map()) -> {ok, binary()} | {error, binary()}
-
-
- -`StorePath`: The store path to update.
`Opts`: The options to update.
- -returns: `{ok, Binary}` on success with operation result message, or -`{error, Binary}` on failure with error message. - -Update the store path to use the mounted volume. - diff --git a/docs/resources/source-code/dev_wasi.md b/docs/resources/source-code/dev_wasi.md index e3f2ce045..488845f51 100644 --- a/docs/resources/source-code/dev_wasi.md +++ b/docs/resources/source-code/dev_wasi.md @@ -1,4 +1,4 @@ -# [Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasi.erl) +# [Module dev_wasi.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasi.erl) diff --git a/docs/resources/source-code/dev_wasm.md b/docs/resources/source-code/dev_wasm.md index 174d97760..0af008177 100644 --- a/docs/resources/source-code/dev_wasm.md +++ b/docs/resources/source-code/dev_wasm.md @@ -1,4 +1,4 @@ -# [Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_wasm.erl) +# [Module dev_wasm.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/preloaded/dev_wasm.erl) diff --git a/docs/resources/source-code/edoc-info b/docs/resources/source-code/edoc-info index 1163d1833..de7252756 100644 --- a/docs/resources/source-code/edoc-info +++ b/docs/resources/source-code/edoc-info @@ -1,17 +1,17 @@ %% encoding: UTF-8 {application,hb}. {modules,[ar_bundles,ar_deep_hash,ar_rate_limiter,ar_timestamp,ar_tx, - ar_wallet,dev_cache,dev_cacheviz,dev_codec_ans104,dev_codec_flat, - dev_codec_httpsig,dev_codec_httpsig_conv,dev_codec_json, - dev_codec_structured,dev_cron,dev_cu,dev_dedup, + ar_wallet,dev_cache,dev_cacheviz,dev_ans104,dev_flat, + dev_httpsig,dev_httpsig_conv,dev_json, + dev_structured,dev_cron,dev_dedup, dev_delegated_compute,dev_faff,dev_genesis_wasm,dev_green_zone, - dev_hook,dev_hyperbuddy,dev_json_iface,dev_local_name,dev_lookup, + dev_hook,dev_hyperbuddy,dev_json_iface,dev_local_name, dev_lua,dev_lua_lib,dev_lua_test,dev_manifest,dev_message,dev_meta, - dev_monitor,dev_multipass,dev_name,dev_node_process,dev_p4, + dev_multipass,dev_name,dev_node_process,dev_p4, dev_patch,dev_poda,dev_process,dev_process_cache,dev_process_worker, dev_push,dev_relay,dev_router,dev_scheduler,dev_scheduler_cache, dev_scheduler_formats,dev_scheduler_registry,dev_scheduler_server, - dev_simple_pay,dev_snp,dev_snp_nif,dev_stack,dev_test,dev_volume, + dev_simple_pay,dev_snp,hb_snp_nif,dev_stack,dev_test, dev_wasi,dev_wasm,hb,hb_ao,hb_ao_test_vectors,hb_app,hb_beamr, hb_beamr_io,hb_cache,hb_cache_control,hb_cache_render,hb_client, hb_crypto,hb_debugger,hb_escape,hb_event,hb_examples,hb_features, @@ -21,4 +21,4 @@ hb_persistent,hb_private,hb_process_monitor,hb_router,hb_singleton, hb_store,hb_store_fs,hb_store_gateway,hb_store_remote_node, hb_store_rocksdb,hb_structured_fields,hb_sup,hb_test_utils, - hb_tracer,hb_util,hb_volume,rsa_pss]}. + hb_tracer,hb_util,rsa_pss]}. diff --git a/docs/resources/source-code/hb_message.md b/docs/resources/source-code/hb_message.md index fd6ee87f4..bbcf25e5f 100644 --- a/docs/resources/source-code/hb_message.md +++ b/docs/resources/source-code/hb_message.md @@ -37,16 +37,16 @@ and O(1) access maps), such that operations upon them are efficient. The structure of the conversions is as follows:
-Arweave TX/ANS-104 ==> dev_codec_ans104:from/1 ==> TABM
-HTTP Signed Message ==> dev_codec_httpsig_conv:from/1 ==> TABM
-Flat Maps ==> dev_codec_flat:from/1 ==> TABM
+Arweave TX/ANS-104 ==> dev_ans104:from/1 ==> TABM
+HTTP Signed Message ==> dev_httpsig_conv:from/1 ==> TABM
+Flat Maps ==> dev_flat:from/1 ==> TABM
 
-TABM ==> dev_codec_structured:to/1 ==> AO-Core Message
-AO-Core Message ==> dev_codec_structured:from/1 ==> TABM
+TABM ==> dev_structured:to/1 ==> AO-Core Message
+AO-Core Message ==> dev_structured:from/1 ==> TABM
 
-TABM ==> dev_codec_ans104:to/1 ==> Arweave TX/ANS-104
-TABM ==> dev_codec_httpsig_conv:to/1 ==> HTTP Signed Message
-TABM ==> dev_codec_flat:to/1 ==> Flat Maps
+TABM ==> dev_ans104:to/1 ==> Arweave TX/ANS-104
+TABM ==> dev_httpsig_conv:to/1 ==> HTTP Signed Message
+TABM ==> dev_flat:to/1 ==> Flat Maps
 ...
 
diff --git a/docs/resources/source-code/dev_snp_nif.md b/docs/resources/source-code/hb_snp_nif.md similarity index 96% rename from docs/resources/source-code/dev_snp_nif.md rename to docs/resources/source-code/hb_snp_nif.md index 34d8e95fd..8dc5841b3 100644 --- a/docs/resources/source-code/dev_snp_nif.md +++ b/docs/resources/source-code/hb_snp_nif.md @@ -1,4 +1,4 @@ -# [Module dev_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/dev_snp_nif.erl) +# [Module hb_snp_nif.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/core/hb_snp_nif.erl) diff --git a/docs/resources/source-code/hb_volume.md b/docs/resources/source-code/hb_volume.md deleted file mode 100644 index 8df22b076..000000000 --- a/docs/resources/source-code/hb_volume.md +++ /dev/null @@ -1,146 +0,0 @@ -# [Module hb_volume.erl](https://github.com/permaweb/HyperBEAM/blob/main/src/hb_volume.erl) - - - - - - -## Function Index ## - - -
change_node_store/2
check_for_device/1
create_actual_partition/2*
create_mount_info/3*
create_partition/2
format_disk/2
get_partition_info/1*
list_partitions/0
mount_disk/4
mount_opened_volume/3*
parse_disk_info/2*
parse_disk_line/2*
parse_disk_model_line/2*
parse_disk_units_line/2*
parse_io_size_line/2*
parse_sector_size_line/2*
process_disk_line/2*
update_store_config/2*
- - - - -## Function Details ## - - - -### change_node_store/2 ### - -

-change_node_store(StorePath::binary(), CurrentStore::list()) -> {ok, map()} | {error, binary()}
-
-
- - - -### check_for_device/1 ### - -

-check_for_device(Device::binary()) -> boolean()
-
-
- - - -### create_actual_partition/2 * ### - -`create_actual_partition(Device, PartType) -> any()` - - - -### create_mount_info/3 * ### - -`create_mount_info(Partition, MountPoint, VolumeName) -> any()` - - - -### create_partition/2 ### - -

-create_partition(Device::binary(), PartType::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### format_disk/2 ### - -

-format_disk(Partition::binary(), EncKey::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### get_partition_info/1 * ### - -`get_partition_info(Device) -> any()` - - - -### list_partitions/0 ### - -

-list_partitions() -> {ok, map()} | {error, binary()}
-
-
- - - -### mount_disk/4 ### - -

-mount_disk(Partition::binary(), EncKey::binary(), MountPoint::binary(), VolumeName::binary()) -> {ok, map()} | {error, binary()}
-
-
- - - -### mount_opened_volume/3 * ### - -`mount_opened_volume(Partition, MountPoint, VolumeName) -> any()` - - - -### parse_disk_info/2 * ### - -`parse_disk_info(Device, Lines) -> any()` - - - -### parse_disk_line/2 * ### - -`parse_disk_line(Line, Info) -> any()` - - - -### parse_disk_model_line/2 * ### - -`parse_disk_model_line(Line, Info) -> any()` - - - -### parse_disk_units_line/2 * ### - -`parse_disk_units_line(Line, Info) -> any()` - - - -### parse_io_size_line/2 * ### - -`parse_io_size_line(Line, Info) -> any()` - - - -### parse_sector_size_line/2 * ### - -`parse_sector_size_line(Line, Info) -> any()` - - - -### process_disk_line/2 * ### - -`process_disk_line(Line, X2) -> any()` - - - -### update_store_config/2 * ### - -

-update_store_config(StoreConfig::term(), NewPath::binary()) -> term()
-
-
- diff --git a/docs/resources/source-code/index.md b/docs/resources/source-code/index.md index e0bb2d593..f009698c4 100644 --- a/docs/resources/source-code/index.md +++ b/docs/resources/source-code/index.md @@ -11,7 +11,7 @@ HyperBEAM is built with a modular architecture to ensure scalability, maintainab - **HyperBEAM Core**: The main framework that orchestrates data processing, storage, and routing. - **Compute Unit**: Handles computational tasks and integrates with the HyperBEAM core for distributed processing. - **Trusted Execution Environment (TEE)**: Ensures secure execution of sensitive operations. -- **Client Libraries**: Tools and SDKs for interacting with HyperBEAM, including the JavaScript client. +- **Client Libraries**: Tools for interacting with HyperBEAM, including the JavaScript client. ## Getting Started @@ -20,4 +20,3 @@ To explore the source code, you can clone the repository from [GitHub](https://g ## Navigation Use the navigation menu to dive into specific parts of the codebase. Each module includes detailed documentation, code comments, and examples to assist in understanding and contributing to the project. - diff --git a/docs/run/configuring-your-machine.md b/docs/run/configuring-your-machine.md index 1154297b4..70a06bb45 100644 --- a/docs/run/configuring-your-machine.md +++ b/docs/run/configuring-your-machine.md @@ -2,30 +2,45 @@ This guide details the various ways to configure your HyperBEAM node's behavior, including ports, storage, keys, and logging. -## Configuration (`config.flat`) +## Configuration (`config.json`) -The primary way to configure your HyperBEAM node is through a `config.flat` file located in the node's working directory or specified by the `HB_CONFIG_LOCATION` environment variable. +The primary way to configure your HyperBEAM node is through a `config.json` file located in the node's working directory or specified by the `HB_CONFIG` environment variable. -This file uses a simple `Key = Value.` format (note the period at the end of each line). +### Flat config file + +Another possibility is to use `config.flat` that uses a simple `Key: Value` format. **Example `config.flat`:** -```erlang +``` % Set the HTTP port -port = 8080. +port: 8080 % Specify the Arweave key file -priv_key_location = "/path/to/your/wallet.json". - -% Set the data store directory -% Note: Storage configuration can be complex. See below. -% store = [{local, [{root, <<"./node_data_mainnet">>}]}]. % Example of complex config, not for config.flat - -% Enable verbose logging for specific modules -% debug_print = [hb_http, dev_router]. % Example of complex config, not for config.flat +priv_key_location: /path/to/your/wallet.json + +% Maps can be used with forward dash (/) +default_store/lmdb/ao-types: store-module=atom +default_store/lmdb/store-module: hb_store_lmdb +default_store/lmdb/name: /tmp/store + +% Lists can be used with dot (.) and sequential integer key map +store/ao-types: .=list +store/1/ao-types: store-module=atom +store/1/store-module: hb_store_lmdb +store/1/name: /tmp/store + +store/2/ao-types: store-module=atom +store/2/store-module: hb_store_s3 +store/2/bucket: hb-s3 +store/2/priv_access_key_id: minioadmin +store/2/priv_secret_access_key: minioadmin +store/2/endpoint: http://localhost:9000 +store/2/force_path_style: true +store/2/region: us-east-1 ``` -Below is a reference of commonly used configuration keys. Remember that `config.flat` only supports simple key-value pairs (Atoms, Strings, Integers, Booleans). For complex configurations (Lists, Maps), you must use environment variables or `hb:start_mainnet/1`. +Below is a reference of commonly used configuration keys. Remember that `config.flat` only supports the following value types (Atoms, Strings, Integers, Booleans, Maps and List). ### Core Configuration @@ -95,7 +110,7 @@ These options control how HyperBEAM manages devices. | Option | Type | Default | Description | |--------|------|---------|-------------| | `load_remote_devices` | Boolean | false | Whether to load devices from remote signers | - + ### Debug & Development diff --git a/erlang_ls.config b/erlang_ls.config index 097464093..f5621bee0 100644 --- a/erlang_ls.config +++ b/erlang_ls.config @@ -18,3 +18,5 @@ lenses: providers: enabled: - signature-help + disabled: + - document-formatting diff --git a/include/hb.hrl b/include/hb.hrl new file mode 100644 index 000000000..3fa8172eb --- /dev/null +++ b/include/hb.hrl @@ -0,0 +1 @@ +-include("../src/core/include/hb.hrl"). diff --git a/install-template b/install-template new file mode 100755 index 000000000..d234383d1 --- /dev/null +++ b/install-template @@ -0,0 +1,208 @@ +#!/usr/bin/env sh +set -eu + +usage() { + cat <<'EOF' +Usage: + install-template [--template-dir PATH] + install-template --local [PATH] [--template-dir PATH] + install-template --branch BRANCH [--repo URL] [--template-dir PATH] + install-template --commit COMMIT [--repo URL] [--template-dir PATH] + +Installs the HyperBEAM Forge `rebar3 new device' template. + +Options: + --local [PATH] Use a local HyperBEAM git checkout. + Defaults to the current working directory. + --branch NAME Use a branch from the HyperBEAM git repository. + Defaults to edge when no source option is given. + --commit SHA Use a commit from the HyperBEAM git repository. + --repo URL Git repository for --branch or --commit. + Defaults to https://github.com/permaweb/hyperbeam.git. + --template-dir PATH Rebar3 template directory. + Defaults to Rebar's user template directory. + -h, --help Show this help. +EOF +} + +die() { + printf '%s\n' "$*" >&2 + exit 1 +} + +quote() { + printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' +} + +git_url() { + case "$1" in + /*) printf 'file://%s' "$1" ;; + *) printf '%s' "$1" ;; + esac +} + +local_ref() { + root="$1" + commit="$(git -C "$root" rev-parse HEAD)" + printf '{ref, "%s"}' "$(quote "$commit")" +} + +script_dir() { + cd "$(dirname "$0")" && pwd +} + +rebar3_config_dir() { + if [ "${XDG_CONFIG_HOME:-}" ]; then + printf '%s/rebar3' "$XDG_CONFIG_HOME" + else + printf '%s/.config/rebar3' "$HOME" + fi +} + +rebar3_env_config_dir() { + case "$1" in + */.config/rebar3) printf '%s' "$1" ;; + *) printf '%s/.config/rebar3' "$1" ;; + esac +} + +default_template_dir() { + if [ "${REBAR_TEMPLATE_DIR:-}" ]; then + printf '%s' "$REBAR_TEMPLATE_DIR" + elif [ "${REBAR_GLOBAL_CONFIG_DIR:-}" ]; then + printf '%s/templates' "$(rebar3_env_config_dir "$REBAR_GLOBAL_CONFIG_DIR")" + elif [ "${REBAR_CACHE_DIR:-}" ]; then + printf '%s/.config/rebar3/templates' "$REBAR_CACHE_DIR" + else + printf '%s/templates' "$(rebar3_config_dir)" + fi +} + +write_device_template() { + path="$1" + hb_dep="$2" + hb_plugin="$3" + prefix="$4" + cat > "$path" <( - env: Env<'a>, - unique_data: Binary, - vmpl: u32, -) -> NifResult> { - log_message("INFO", file!(), line!(), "Starting attestation report generation..."); - - // Step 1: Convert the binary input to a fixed-size array. - let unique_data_array: [u8; 64] = match unique_data.as_slice().try_into() { - Ok(data) => data, - Err(_) => { - let msg = "Input binary must be exactly 64 bytes long."; - log_message("ERROR", file!(), line!(), msg); - return Err(rustler::Error::BadArg); - } - }; - - // Step 2: Open the firmware interface. - let mut firmware = match Firmware::open() { - Ok(fw) => { - log_message("INFO", file!(), line!(), "Firmware opened successfully."); - fw - } - Err(err) => { - let msg = format!("Failed to open firmware: {:?}", err); - log_message("ERROR", file!(), line!(), &msg); - return Ok((atom::error(), msg).encode(env)); - } - }; - - // Step 3: Generate the attestation report. - let report: AttestationReport = match firmware.get_report(None, Some(unique_data_array), Some(vmpl)) { - Ok(report) => { - log_message("INFO", file!(), line!(), "Attestation report generated successfully."); - report - } - Err(err) => { - let msg = format!("Failed to generate attestation report: {:?}", err); - log_message("ERROR", file!(), line!(), &msg); - return Ok((atom::error(), msg).encode(env)); - } - }; - - // Step 4: Serialize the report into a JSON string for output. - let report_json = match to_string(&report) { - Ok(json) => { - log_message("INFO", file!(), line!(), "Attestation report serialized to JSON format."); - json - } - Err(err) => { - let msg = format!("Failed to serialize attestation report: {:?}", err); - log_message("ERROR", file!(), line!(), &msg); - return Ok((atom::error(), msg).encode(env)); - } - }; - - // Step 5: Log the serialized JSON for debugging purposes. - log_message( - "INFO", - file!(), - line!(), - &format!("Generated report JSON: {:?}", report_json), - ); - - // Step 6: Return the result as a tuple with the `ok` atom. - Ok((ok(), report_json).encode(env)) -} diff --git a/native/dev_snp_nif/src/digest.rs b/native/dev_snp_nif/src/digest.rs deleted file mode 100644 index 5adb3bcb9..000000000 --- a/native/dev_snp_nif/src/digest.rs +++ /dev/null @@ -1,148 +0,0 @@ -use rustler::{Encoder, Env, MapIterator, NifResult, Term}; -use rustler::types::atom::{self, ok}; -use sev::measurement::snp::{snp_calc_launch_digest, SnpMeasurementArgs}; -use sev::measurement::vcpu_types::CpuType; -use sev::measurement::vmsa::{GuestFeatures, VMMType}; -use crate::logging::log_message; -use std::path::PathBuf; -use bincode; - -/// Struct to hold launch digest arguments passed from Erlang -#[derive(Debug)] -struct LaunchDigestArgs { - vcpus: u32, - vcpu_type: u8, - vmm_type: u8, - guest_features: u64, - ovmf_hash_str: String, - kernel_hash: String, - initrd_hash: String, - append_hash: String, -} - -/// Computes the launch digest using the input arguments provided as an Erlang map. -/// -/// # Arguments -/// * `env` - The Rustler environment, used to encode the return value. -/// * `input_map` - An Erlang map containing the input parameters required for the calculation. -/// -/// # Returns -/// A tuple containing an `ok` atom and the calculated and serialized launch digest. -/// If the input is invalid or an error occurs during calculation, an error is returned. -/// -/// # Expected Input Map Keys: -/// - `"vcpus"`: Number of virtual CPUs (u32). -/// - `"vcpu_type"`: Type of the virtual CPU (u8). -/// - `"vmm_type"`: Type of the Virtual Machine Monitor (u8). -/// - `"guest_features"`: Features of the guest (u64). -/// - `"ovmf_hash_str"`: Hash of the OVMF firmware (String). -/// - `"kernel_hash"`: Hash of the kernel (String). -/// - `"initrd_hash"`: Hash of the initrd (String). -/// - `"append_hash"`: Hash of the kernel command line arguments (String). -/// -/// # Example -/// ```erlang -/// {ok, LaunchDigest} = dev_snp_nif:compute_launch_digest(InputMap). -/// ``` -#[rustler::nif] -pub fn compute_launch_digest<'a>(env: Env<'a>, input_map: Term<'a>) -> NifResult> { - //log_message("INFO", file!(), line!(), "Starting launch digest calculation..."); - - // Step 1: Validate that the input is a map. - if !input_map.is_map() { - log_message("ERROR", file!(), line!(), "Provided input is not a map."); - return Err(rustler::Error::BadArg); - } - - // Step 2: Helper function to decode string values from the map. - fn decode_string(value: Term) -> NifResult { - match value.get_type() { - rustler::TermType::List => { - let list: Vec = value.decode()?; - String::from_utf8(list).map_err(|_| rustler::Error::BadArg) - } - _ => value.decode(), - } - } - - // Step 3: Parse input map into LaunchDigestArgs. - let mut args = LaunchDigestArgs { - vcpus: 0, - vcpu_type: 0, - vmm_type: 0, - guest_features: 0, - ovmf_hash_str: String::new(), - kernel_hash: String::new(), - initrd_hash: String::new(), - append_hash: String::new(), - }; - - let map_iter = MapIterator::new(input_map).unwrap(); - for (key, value) in map_iter { - let key_str = key.atom_to_string()?.to_string(); - match key_str.as_str() { - "vcpus" => args.vcpus = value.decode()?, - "vcpu_type" => args.vcpu_type = value.decode()?, - "vmm_type" => args.vmm_type = value.decode()?, - "guest_features" => args.guest_features = value.decode()?, - "firmware" => args.ovmf_hash_str = decode_string(value)?, - "kernel" => args.kernel_hash = decode_string(value)?, - "initrd" => args.initrd_hash = decode_string(value)?, - "append" => args.append_hash = decode_string(value)?, - _ => log_message("WARN", file!(), line!(), &format!("Unexpected key: {}", key_str)), - } - } - - //log_message("INFO", file!(), line!(), &format!("Parsed arguments: {:?}", args)); - - // Step 4: Prepare SnpMeasurementArgs for digest calculation. - let ovmf_file = "test/OVMF-1.55.fd".to_owned(); - let measurement_args = SnpMeasurementArgs { - ovmf_file: Some(PathBuf::from(ovmf_file)), - kernel_file: None, - initrd_file: None, - append: None, - // vcpus: args.vcpus, - // vcpu_type: CpuType::try_from(args.vcpu_type).unwrap(), - // vmm_type: Some(VMMType::try_from(args.vmm_type).unwrap()), - // guest_features: GuestFeatures(args.guest_features), - vcpus: 32, - vcpu_type: CpuType::EpycV4, - vmm_type: Some(VMMType::QEMU), - guest_features: GuestFeatures(0x1), - ovmf_hash_str: Some(args.ovmf_hash_str.as_str()), - kernel_hash: Some(hex::decode(args.kernel_hash).unwrap().try_into().unwrap()), - initrd_hash: Some(hex::decode(args.initrd_hash).unwrap().try_into().unwrap()), - append_hash: Some(hex::decode(args.append_hash).unwrap().try_into().unwrap()), - }; - - // Step 5: Compute the launch digest. - let digest = match snp_calc_launch_digest(measurement_args) { - Ok(digest) => digest, - Err(err) => { - let msg = format!("Failed to compute launch digest: {:?}", err); - log_message("ERROR", file!(), line!(), &msg); - return Ok((atom::error(), msg).encode(env)); - } - }; - - // Step 6: Serialize the digest. - let serialized_digest = match bincode::serialize(&digest) { - Ok(serialized) => serialized, - Err(err) => { - let msg = format!("Failed to serialize launch digest: {:?}", err); - log_message("ERROR", file!(), line!(), &msg); - return Ok((atom::error(), msg).encode(env)); - } - }; - - //log_message( - // "INFO", - // file!(), - // line!(), - // "Launch digest successfully computed and serialized.", - //); - - // Step 7: Return the calculated and serialized digest. - Ok((ok(), serialized_digest).encode(env)) -} diff --git a/native/dev_snp_nif/src/helpers.rs b/native/dev_snp_nif/src/helpers.rs deleted file mode 100644 index b74482264..000000000 --- a/native/dev_snp_nif/src/helpers.rs +++ /dev/null @@ -1,110 +0,0 @@ -use sev::certs::snp::{ca, Certificate}; -use sev::firmware::host::TcbVersion; -use crate::logging::log_message; -use reqwest::blocking::get; - -/// Base URL for AMD's Key Distribution Service (KDS). -const KDS_CERT_SITE: &str = "https://kdsintf.amd.com"; -/// Endpoint for the VCEK API. -const KDS_VCEK: &str = "/vcek/v1"; -/// Endpoint for the Certificate Chain API. -const KDS_CERT_CHAIN: &str = "cert_chain"; - -/// Requests the AMD certificate chain (ASK + ARK) for the given SEV product name. -/// -/// # Arguments -/// * `sev_prod_name` - The SEV product name (e.g., "Milan"). -/// -/// # Returns -/// A `ca::Chain` containing the ASK and ARK certificates. -/// -/// # Errors -/// Returns an error if the request fails, the response is invalid, or the certificate parsing fails. -/// -/// # Example -/// ```erlang -/// {ok, CertChain} = dev_snp_nif:request_cert_chain("Milan"). -pub fn request_cert_chain(sev_prod_name: &str) -> Result> { -// Blocking version of reqwest - let url = format!("{KDS_CERT_SITE}{KDS_VCEK}/{sev_prod_name}/{KDS_CERT_CHAIN}"); - // log_message( - // "INFO", - // file!(), - // line!(), - // &format!("Requesting AMD certificate chain from: {url}"), - // ); - - // Perform the blocking GET request - let response = get(&url)?; - let body = response.bytes()?; - - // Parse the response as a PEM-encoded certificate chain - let chain = openssl::x509::X509::stack_from_pem(&body)?; - if chain.len() < 2 { - return Err("Expected at least two certificates (ARK and ASK) in the chain".into()); - } - - // Convert ARK and ASK into the `ca::Chain` structure required by the SEV crate - let ark = chain[1].to_pem()?; - let ask = chain[0].to_pem()?; - let ca_chain = ca::Chain::from_pem(&ark, &ask)?; - - //log_message( - // "INFO", - // file!(), - // line!(), - // "Successfully fetched AMD certificate chain.", - //); - - Ok(ca_chain) -} - -/// Requests the VCEK for the given chip ID and reported TCB. -/// -/// # Arguments -/// * `chip_id` - The unique 64-byte chip ID. -/// * `reported_tcb` - The TCB version of the platform. -/// -/// # Returns -/// A `Certificate` representing the VCEK. -/// -/// # Errors -/// Returns an error if the request fails, the response is invalid, or the certificate parsing fails. -/// -/// # Example -/// ```erlang -/// {ok, VcekCert} = dev_snp_nif:request_vcek(ChipIdBinary, ReportedTcbMap). -/// ``` -pub fn request_vcek( - chip_id: [u8; 64], - reported_tcb: TcbVersion, -) -> Result> { - use reqwest::blocking::get; // Blocking version of reqwest - - let hw_id = chip_id - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::(); - - let url = format!( - "{KDS_CERT_SITE}{KDS_VCEK}/Milan/{hw_id}?blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}", - reported_tcb.bootloader, reported_tcb.tee, reported_tcb.snp, reported_tcb.microcode - ); - - // log_message( - // "INFO", - // file!(), - // line!(), - // &format!("Requesting VCEK from: {url}"), - // ); - - // Perform the blocking GET request - let response = get(&url)?; - let rsp_bytes = response.bytes()?; - - // Parse the VCEK response as a DER-encoded certificate - let vcek_cert = Certificate::from_der(&rsp_bytes)?; - - // log_message("INFO", file!(), line!(), "Successfully fetched VCEK."); - Ok(vcek_cert) -} diff --git a/native/dev_snp_nif/src/lib.rs b/native/dev_snp_nif/src/lib.rs deleted file mode 100644 index abfc92abc..000000000 --- a/native/dev_snp_nif/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -/// Entry point for the Rustler NIF module. -/// This file defines the available NIF functions and organizes them into modules. - -mod logging; -mod snp_support; -mod attestation; -mod digest; -mod verification; -mod helpers; - -rustler::init!( - "dev_snp_nif"// Module name as used in Erlang. -); diff --git a/native/dev_snp_nif/src/logging.rs b/native/dev_snp_nif/src/logging.rs deleted file mode 100644 index 31be106fa..000000000 --- a/native/dev_snp_nif/src/logging.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::thread; -use std::time::SystemTime; - -/// Logs messages with details including thread ID, timestamp, file, and line number. -/// -/// # Arguments -/// - `log_level`: The log level (e.g., "INFO", "ERROR"). -/// - `file`: The file where the log is being generated. -/// - `line`: The line number of the log statement. -/// - `message`: The log message. -/// -/// # Example -/// ```rust -/// log_message("INFO", file!(), line!(), "This is a log message."); -/// ``` -pub fn log_message(log_level: &str, file: &str, line: u32, message: &str) { - let thread_id = thread::current().id(); - let now = SystemTime::now(); - let timestamp = now - .duration_since(SystemTime::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - - println!( - "[{}#{:?} @ {}:{}] [{}] {}", - log_level, thread_id, file, line, timestamp, message - ); -} diff --git a/native/dev_snp_nif/src/snp_support.rs b/native/dev_snp_nif/src/snp_support.rs deleted file mode 100644 index 0ca9da69c..000000000 --- a/native/dev_snp_nif/src/snp_support.rs +++ /dev/null @@ -1,44 +0,0 @@ -use rustler::{Encoder, Env, NifResult, Term}; -use rustler::types::atom::ok; -use sev::firmware::guest::Firmware; -use crate::logging::log_message; - -/// Checks if Secure Nested Paging (SNP) is supported by the system. -/// -/// # Arguments -/// * `env` - The Rustler environment, used to encode the return value. -/// -/// # Returns -/// A tuple containing an `ok` atom and a boolean value: -/// - `true` if the firmware indicates that SNP is supported. -/// - `false` if SNP is not supported or if the firmware cannot be accessed. -/// -/// # Example -/// ```erlang -/// {ok, Supported} = dev_snp_nif:check_snp_support(). -/// ``` -#[rustler::nif] -pub fn check_snp_support<'a>(env: Env<'a>) -> NifResult> { - //log_message("INFO", file!(), line!(), "Checking SNP support..."); - - // Step 1: Attempt to open the firmware interface. - // If the firmware is accessible, SNP is supported; otherwise, it is not. - let is_supported = match Firmware::open() { - Ok(_) => { - //log_message("INFO", file!(), line!(), "SNP is supported."); - true // SNP is supported. - } - Err(_) => { - // log_message( - // "ERROR", - // file!(), - // line!(), - // "Failed to open firmware. SNP is not supported.", - // ); - false // SNP is not supported. - } - }; - - // Step 2: Return the result as a tuple with the `ok` atom and the boolean value. - Ok((ok(), is_supported).encode(env)) -} diff --git a/native/dev_snp_nif/src/verification.rs b/native/dev_snp_nif/src/verification.rs deleted file mode 100644 index e8636e851..000000000 --- a/native/dev_snp_nif/src/verification.rs +++ /dev/null @@ -1,310 +0,0 @@ -use rustler::{Binary, Encoder, Env, NifResult, Term}; -use rustler::types::atom::{self, ok}; -use serde_json::Value; -use serde::Deserialize; -use sev::certs::snp::{ecdsa::Signature, Chain, Verifiable}; -use sev::firmware::host::TcbVersion; -use sev::firmware::guest::{AttestationReport, GuestPolicy, PlatformInfo}; -use crate::helpers::{request_cert_chain, request_vcek}; -use crate::logging::log_message; - -/// Verifies whether the measurement in the attestation report matches the expected measurement. -/// -/// # Arguments -/// * `env` - The Rustler environment, used to encode the return value. -/// * `_report` - A binary containing the serialized attestation report (JSON format). -/// * `_expected_measurement` - A binary containing the expected measurement (as a byte array). -/// -/// # Returns -/// A tuple with: -/// - `ok` atom and a success message if the measurements match. -/// - `error` atom and an error message if the measurements do not match. -#[rustler::nif] -fn verify_measurement<'a>( - env: Env<'a>, - _report: Binary, - _expected_measurement: Binary, -) -> NifResult> { - //log_message("INFO", file!(), line!(), "Starting measurement verification..."); - - // Define a struct for deserializing the attestation report. - #[derive(Debug, Deserialize)] - struct AttestationReport { - measurement: Vec, - // Additional fields can be added here if needed. - } - - // Step 1: Deserialize the JSON report. - let report: AttestationReport = match serde_json::from_slice(_report.as_slice()) { - Ok(parsed_report) => { - //log_message( - // "INFO", - // file!(), - // line!(), - // &format!("Successfully parsed report: {:?}", parsed_report), - //); - parsed_report - } - Err(err) => { - log_message( - "ERROR", - file!(), - line!(), - &format!("Failed to deserialize report: {:?}", err), - ); - return Ok((atom::error(), "Invalid report format").encode(env)); - } - }; - - // Step 2: Extract the actual measurement from the report. - let actual_measurement = &report.measurement; - // log_message( - // "INFO", - // file!(), - // line!(), - // &format!("Extracted actual measurement: {:?}", actual_measurement), - // ); - - // Step 3: Decode the expected measurement from the input binary. - let expected_measurement: Vec = _expected_measurement.as_slice().to_vec(); - // log_message( - // "INFO", - // file!(), - // line!(), - // &format!("Decoded expected measurement: {:?}", expected_measurement), - // ); - - // Step 4: Compare the actual and expected measurements. - if actual_measurement == &expected_measurement { - //log_message("INFO", file!(), line!(), "Measurements match."); - Ok((atom::ok(), true).encode(env)) - } else { - //log_message("ERROR", file!(), line!(), "Measurements do not match."); - Ok((atom::error(), false).encode(env)) - } -} - - -/// Verifies the signature of an attestation report. -/// -/// # Arguments -/// * `env` - The Rustler environment, used to encode the return value. -/// * `report` - A binary containing the serialized attestation report. -/// -/// # Returns -/// A tuple with: -/// - `ok` atom and a success message if the signature is valid. -/// - `error` atom and an error message if the signature verification fails. -#[rustler::nif] -fn verify_signature<'a>( - env: Env<'a>, - report: Binary<'a>, -) -> NifResult> { - // log_message("INFO", file!(), line!(), "Verifying signature..."); - - // Step 1: Parse the report JSON into a serde Value object. - let json_data = match serde_json::from_slice::(report.as_slice()) { - Ok(data) => data, - Err(err) => { - return Ok(( - rustler::types::atom::error(), - format!("Failed to parse JSON: {}", err), - ) - .encode(env)); - } - }; - - // Step 2: Map JSON fields to the AttestationReport struct. - // Each field is individually parsed to ensure type safety. - let attestation_report = AttestationReport { - version: json_data["version"].as_u64().unwrap_or(0) as u32, - guest_svn: json_data["guest_svn"].as_u64().unwrap_or(0) as u32, - policy: GuestPolicy(json_data["policy"].as_u64().unwrap_or(0)), - family_id: json_data["family_id"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 16]), - image_id: json_data["image_id"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 16]), - vmpl: json_data["vmpl"].as_u64().unwrap_or(0) as u32, - sig_algo: json_data["sig_algo"].as_u64().unwrap_or(0) as u32, - current_tcb: TcbVersion { - bootloader: json_data["current_tcb"]["bootloader"].as_u64().unwrap_or(0) as u8, - tee: json_data["current_tcb"]["tee"].as_u64().unwrap_or(0) as u8, - snp: json_data["current_tcb"]["snp"].as_u64().unwrap_or(0) as u8, - microcode: json_data["current_tcb"]["microcode"].as_u64().unwrap_or(0) as u8, - _reserved: [0; 4], - }, - plat_info: PlatformInfo(json_data["plat_info"].as_u64().unwrap_or(0)), - _author_key_en: json_data["_author_key_en"].as_u64().unwrap_or(0) as u32, - _reserved_0: json_data["_reserved_0"].as_u64().unwrap_or(0) as u32, - report_data: json_data["report_data"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 64]), - measurement: json_data["measurement"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 48]), - host_data: json_data["host_data"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 32]), - id_key_digest: json_data["id_key_digest"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 48]), - author_key_digest: json_data["author_key_digest"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 48]), - report_id: json_data["report_id"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 32]), - report_id_ma: json_data["report_id_ma"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 32]), - reported_tcb: TcbVersion { - bootloader: json_data["reported_tcb"]["bootloader"] - .as_u64() - .unwrap_or(0) as u8, - tee: json_data["reported_tcb"]["tee"].as_u64().unwrap_or(0) as u8, - snp: json_data["reported_tcb"]["snp"].as_u64().unwrap_or(0) as u8, - microcode: json_data["reported_tcb"]["microcode"].as_u64().unwrap_or(0) as u8, - _reserved: [0; 4], - }, - _reserved_1: [0; 24], - chip_id: json_data["chip_id"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 64]), - committed_tcb: TcbVersion { - bootloader: json_data["committed_tcb"]["bootloader"] - .as_u64() - .unwrap_or(0) as u8, - tee: json_data["committed_tcb"]["tee"].as_u64().unwrap_or(0) as u8, - snp: json_data["committed_tcb"]["snp"].as_u64().unwrap_or(0) as u8, - microcode: json_data["committed_tcb"]["microcode"] - .as_u64() - .unwrap_or(0) as u8, - _reserved: [0; 4], - }, - current_build: json_data["current_build"].as_u64().unwrap_or(0) as u8, - current_minor: json_data["current_minor"].as_u64().unwrap_or(0) as u8, - current_major: json_data["current_major"].as_u64().unwrap_or(0) as u8, - _reserved_2: json_data["_reserved_2"].as_u64().unwrap_or(0) as u8, - committed_build: json_data["committed_build"].as_u64().unwrap_or(0) as u8, - committed_minor: json_data["committed_minor"].as_u64().unwrap_or(0) as u8, - committed_major: json_data["committed_major"].as_u64().unwrap_or(0) as u8, - _reserved_3: json_data["_reserved_3"].as_u64().unwrap_or(0) as u8, - launch_tcb: TcbVersion { - bootloader: json_data["launch_tcb"]["bootloader"].as_u64().unwrap_or(0) as u8, - tee: json_data["launch_tcb"]["tee"].as_u64().unwrap_or(0) as u8, - snp: json_data["launch_tcb"]["snp"].as_u64().unwrap_or(0) as u8, - microcode: json_data["launch_tcb"]["microcode"].as_u64().unwrap_or(0) as u8, - _reserved: [0; 4], - }, - _reserved_4: [0; 168], - signature: Signature { - r: json_data["signature"]["r"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 72]), - s: json_data["signature"]["s"] - .as_array() - .unwrap_or(&vec![]) - .iter() - .map(|v| v.as_u64().unwrap_or(0) as u8) - .collect::>() - .try_into() - .unwrap_or([0; 72]), - _reserved: [0; 368], - }, - }; - - // Step 3: Extract the chip ID and TCB version. - let chip_id_array: [u8; 64] = attestation_report - .chip_id - .try_into() - .expect("chip_id must be 64 bytes"); - let tcb_version = attestation_report.current_tcb; - - // Step 4: Request the certificate chain and VCEK. - let ca = request_cert_chain("Milan").unwrap(); - let vcek = request_vcek(chip_id_array, tcb_version).unwrap(); - - // Step 5: Verify the certificate chain. - if let Err(e) = ca.verify() { - log_message( - "ERROR", - file!(), - line!(), - &format!("CA chain verification failed: {:?}", e), - ); - return Ok((atom::error(), format!("CA verification failed: {:?}", e)).encode(env)); - } - //log_message("INFO", file!(), line!(), "CA chain verification successful."); - - // Step 6: Verify the attestation report. - let cert_chain = Chain { ca, vek: vcek }; - if let Err(e) = (&cert_chain, &attestation_report).verify() { - log_message( - "ERROR", - file!(), - line!(), - &format!("Attestation report verification failed: {:?}", e), - ); - return Ok((atom::error(), format!("Report verification failed: {:?}", e)).encode(env)); - } - - //log_message("INFO", file!(), line!(), "Signature verification successful."); - Ok((ok(), true).encode(env)) -} diff --git a/native/digest_calc/.gitignore b/native/digest_calc/.gitignore deleted file mode 100644 index be2bbcfd0..000000000 --- a/native/digest_calc/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -files -target -Cargo.lock \ No newline at end of file diff --git a/native/digest_calc/Cargo.toml b/native/digest_calc/Cargo.toml deleted file mode 100644 index f58f90b5b..000000000 --- a/native/digest_calc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "digest_calc" -version = "0.1.0" -edition = "2021" - -[dependencies] -sev = { git = "https://github.com/PeterFarber/sev.git", features = ["openssl"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.9.34" -openssl = "0.10.66" -bincode = "1.3" -clap = "3.0" \ No newline at end of file diff --git a/native/digest_calc/config.yml b/native/digest_calc/config.yml deleted file mode 100644 index 8ba31931f..000000000 --- a/native/digest_calc/config.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Kernel configuration file path -kernel_file: "/home/peterfarber/_Current/HyperBEAM/native/digest_calc/files/kernel" # Path to the kernel binary - -# Initrd configuration file path -initrd_file: "/home/peterfarber/_Current/HyperBEAM/native/digest_calc/files/initrd" # Path to the initrd (initial RAM disk) - -# OVMF (Open Virtual Machine Firmware) file path -ovmf_file: "/home/peterfarber/_Current/HyperBEAM/native/digest_calc/files/ovmf" # Path to the OVMF file used for virtual machine boot - -# Kernel command line arguments -cmdline: "console=ttyS0 earlyprintk=serial root=/dev/sda boot=verity verity_disk=/dev/sdb verity_roothash=7270de8ae229d0e8c219170b2c8b34d20d544d74f77c9469b81d22b1697ad3aa" -# Kernel boot arguments including console settings, verity disk and root hash for secure boot - -# Number of virtual CPUs to be allocated for the virtual machine -vcpus: 1 # Set to 1 for a single virtual CPU (adjust as necessary) - -# VCPU Types: List of available virtual CPU models. These are typically based on physical CPU models. -# Each of these types corresponds to a specific configuration of CPU features that the virtual machine will use. -# Examples include specific features for hardware virtualization, optimizations, or security configurations. - -vcpu_type: "EpycV4" # Choose the CPU model for the virtual machine (EpycV4 is a modern AMD CPU type) - -# Virtual Machine Monitor (VMM) Types: Specifies the hypervisor used for the VM. -# - QEMU: A generic and widely used open-source hypervisor. -# - EC2: Amazon's EC2 instance type for cloud-based VMs. -# - KRUN: A specialized hypervisor used in specific security and research contexts. -vmm_type: "QEMU" # Choose the type of hypervisor. QEMU is commonly used for local virtual machines. - -# Guest Features: -# The guest features are represented by individual bits in a 64-bit integer. -# Each bit in the 64-bit integer corresponds to an enabled or disabled feature for the virtual machine. -# The bits can be set to '1' to enable a specific feature, or '0' to disable it. - -# Here's the breakdown of the available bits: - -# | Bit | Feature | -# |-----|----------------------| -# | 0 | SNPActive | # Enables Secure Nested Paging (SNP), enhancing security for the VM -# | 1 | vTOM | # Enables virtual Trusted Opaque Memory, a security feature -# | 2 | ReflectVC | # Enables Reflection for VC (Virtualization Context) -# | 3 | RestrictedInjection | # Restricts certain types of injection into the VM -# | 4 | AlternateInjection | # Enables alternate forms of injection into the VM -# | 5 | DebugSwap | # Allows for debugging VM swap operations -# | 6 | PreventHostIBS | # Prevents Instruction-Based Sampling on the host system -# | 7 | BTBIsolation | # Isolates Branch Target Buffer for security -# | 8 | VmplSSS | # Enables Virtual Memory for Secure State (SSS) support -# | 9 | SecureTSC | # Enables Secure Time Stamp Counter, preventing time-based attacks -# | 10 | VmgexitParameter | # Enables parameters related to VM exit for performance tuning -# | 11 | Reserved, SBZ | # Reserved bit, should not be used, always zero -# | 12 | IbsVirtualization | # Allows Ibs (Interrupt-based Sampling) virtualization -# | 13 | Reserved, SBZ | # Reserved bit, should not be used, always zero -# | 14 | VmsaRegProt | # Enables VM-Sensitive Register Protection -# | 15 | SmtProtection | # Protects against Simultaneous Multithreading (SMT) attacks - -# The value is represented as a hexadecimal string where each bit corresponds to a feature. -# For example, a value of "0000000000000001" means: -# - Bit 0 (SNPActive) is enabled. -# All other bits are set to 0. - -guest_features: "0000000000000001" # Bit 0: SNPActive Enabled (secure virtualization enabled) - -# Notes: -# - You can adjust this value to enable or disable features by modifying the appropriate bits. -# - To turn on additional features, set the corresponding bit to '1' (e.g., "0000000000000011" to enable SNPActive and vTOM). diff --git a/native/digest_calc/src/main.rs b/native/digest_calc/src/main.rs deleted file mode 100644 index aba6bd5be..000000000 --- a/native/digest_calc/src/main.rs +++ /dev/null @@ -1,358 +0,0 @@ -// This program calculates the SEV-SNP launch digest, which is used for -// verifying the integrity of a virtual machine at launch. It computes -// cryptographic hashes of the kernel, initrd, cmdline, and OVMF files, -// and generates the corresponding launch digest required for secure attestation -// in SEV-SNP environments. - -use bincode; -use clap::{App, Arg}; -use serde::{Deserialize, Serialize}; -use sev::error::MeasurementError; -use sev::measurement::sev_hashes::SevHashes; -use sev::measurement::snp::{ - calc_snp_ovmf_hash, snp_calc_launch_digest, SnpLaunchDigest, SnpMeasurementArgs, -}; -use sev::measurement::vcpu_types::CpuType; -use sev::measurement::vmsa::{GuestFeatures, VMMType}; -use std::fs; -use std::path::PathBuf; - -/// Struct to hold the arguments received from the command line. -#[derive(Serialize)] -struct Arguments { - config: Option, // Path to the configuration file - kernel_file: Option, // Path to the kernel file - initrd_file: Option, // Path to the initrd file - ovmf_file: Option, // Path to the OVMF file - cmdline: Option, // Kernel command line - vcpus: Option, // Number of virtual CPUs - vcpu_type: Option, // Type of virtual CPU - vmm_type: Option, // Virtual Machine Monitor type - guest_features: Option, // Guest features as a hex value -} - -/// Struct to hold the configuration loaded from a YAML file. -#[derive(Debug, Deserialize, Serialize)] -struct Config { - kernel_file: String, - initrd_file: String, - ovmf_file: String, - cmdline: String, - vcpus: Option, - vcpu_type: Option, - vmm_type: Option, - guest_features: Option, -} - -/// Converts a byte slice to a hexadecimal string representation. -fn bytes_to_hex(bytes: &[u8]) -> String { - bytes.iter().map(|byte| format!("{:02x}", byte)).collect() -} - -/// Calculates the launch measurement digest using the SEV-SNP arguments. -fn calculate_launch_measurment( - snp_measure_args: SnpMeasurementArgs, -) -> Result<[u8; 384 / 8], String> { - // Calculate the launch digest - let ld = snp_calc_launch_digest(snp_measure_args) - .map_err(|e| format!("Failed to compute launch digest: {:?}", e))?; - - // Serialize the launch digest - let ld_vec = bincode::serialize(&ld).map_err(|e| { - format!( - "Failed to bincode serialize SnpLaunchDigest to Vec: {:?}", - e - ) - })?; - - // Convert the serialized data into a fixed-length byte array - let ld_arr: [u8; 384 / 8] = ld_vec - .try_into() - .map_err(|_| "SnpLaunchDigest has unexpected length".to_string())?; - - Ok(ld_arr) -} - -/// Calculates the OVMF file hash. -pub fn get_ovmf_hash_from_file(ovmf_file: PathBuf) -> Result { - calc_snp_ovmf_hash(ovmf_file) -} - -/// Retrieves the hashes for kernel, initrd, and cmdline files. -pub fn get_hashes_from_files( - kernel_file: PathBuf, - initrd_file: Option, - append: Option<&str>, -) -> Result { - SevHashes::new(kernel_file, initrd_file, append) -} - -fn main() { - // Starting message - println!("=== Digest Calculator Starting ==="); - - println!("\n=== Getting Command Line Arguments ==="); - // Parse command line arguments using the clap library - let matches = App::new("SEV SNP Measurement") - .version("1.0") - .author("Peter Farber ") - .about( - "SEV-SNP Launch Digest Calculation\n\n\ - Example commands:\n\ - 1. Basic example with default values:\n\ - ./sev_snp_measurement --kernel_file /path/to/kernel --ovmf_file /path/to/ovmf --cmdline \"root=/dev/sda console=ttyS0\"\n\ - 2. Specify all arguments, including optional ones:\n\ - ./sev_snp_measurement --kernel_file /path/to/kernel --initrd_file /path/to/initrd --ovmf_file /path/to/ovmf --cmdline \"root=/dev/sda console=ttyS0\" --vcpus 2 --vcpu_type EpycV4 --vmm_type QEMU --guest_features 0x1\n\ - 3. Use a different VMM type and guest features:\n\ - ./sev_snp_measurement --kernel_file /path/to/kernel --ovmf_file /path/to/ovmf --cmdline \"root=/dev/sda console=ttyS0\" --vcpus 4 --vcpu_type EpycMilan --vmm_type EC2 --guest_features 0x2\n" - ) - .arg(Arg::new("config") - .help("Path to the YAML configuration file") - .takes_value(true)) - .arg(Arg::new("kernel_file") - .help("The path to the kernel file (required)") - .required_unless_present("config") - .takes_value(true)) - .arg(Arg::new("initrd_file") - .help("The path to the initrd file (required)") - .required_unless_present("config") - .takes_value(true)) - .arg(Arg::new("ovmf_file") - .help("The path to the OVMF file (required)") - .required_unless_present("config") - .takes_value(true)) - .arg(Arg::new("cmdline") - .help("The kernel command line (required)") - .required_unless_present("config") - .takes_value(true)) - .arg(Arg::new("vcpus") - .help("Number of virtual CPUs (default: 1)") - .takes_value(true) - .default_value("1")) - .arg(Arg::new("vcpu_type") - .help("The type of virtual CPU (default: EpycV4)\n\ - Available options:\n\ - \"Epyc\" => CpuType::Epyc,\n\ - \"EpycV1\" => CpuType::EpycV1,\n\ - \"EpycV2\" => CpuType::EpycV2,\n\ - \"EpycIBPB\" => CpuType::EpycIBPB,\n\ - \"EpycV3\" => CpuType::EpycV3,\n\ - \"EpycV4\" => CpuType::EpycV4,\n\ - \"EpycRome\" => CpuType::EpycRome,\n\ - \"EpycRomeV1\" => CpuType::EpycRomeV1,\n\ - \"EpycRomeV2\" => CpuType::EpycRomeV2,\n\ - \"EpycRomeV3\" => CpuType::EpycRomeV3,\n\ - \"EpycMilan\" => CpuType::EpycMilan,\n\ - \"EpycMilanV1\" => CpuType::EpycMilanV1,\n\ - \"EpycMilanV2\" => CpuType::EpycMilanV2,\n\ - \"EpycGenoa\" => CpuType::EpycGenoa,\n\ - \"EpycGenoaV1\" => CpuType::EpycGenoaV1") - .takes_value(true) - .default_value("EpycV4")) - .arg(Arg::new("vmm_type") - .help("The VMM type (default: QEMU)\n\ - Available options:\n\ - \"QEMU\" => Some(VMMType::QEMU),\n\ - \"EC2\" => Some(VMMType::EC2),\n\ - \"KRUN\" => Some(VMMType::KRUN),") - .takes_value(true) - .default_value("QEMU")) - .arg(Arg::new("guest_features") - .help("Guest features as a hex value (default: 0x1)\n\ - Available features:\n\ - | 0 | SNPActive |\n\ - | 1 | vTOM |\n\ - | 2 | ReflectVC |\n\ - | 3 | RestrictedInjection |\n\ - | 4 | AlternateInjection |\n\ - | 5 | DebugSwap |\n\ - | 6 | PreventHostIBS |\n\ - | 7 | BTBIsolation |\n\ - | 8 | VmplSSS |\n\ - | 9 | SecureTSC |\n\ - | 10 | VmgexitParameter |\n\ - | 11 | Reserved, SBZ |\n\ - | 12 | IbsVirtualization |\n\ - | 13 | Reserved, SBZ |\n\ - | 14 | VmsaRegProt |\n\ - | 15 | SmtProtection |\n\ - | 63:16 | Reserved, SBZ |\n\n\ - Example Usage:\n\ - 1. Enable SNPActive (bit 0):\n\ - guest_features 0000000000000001\n") - .takes_value(true) - .default_value("0x1")) - .get_matches(); - - // Store the parsed command line arguments - let args = Arguments { - config: matches.value_of("config").map(String::from), - kernel_file: matches.value_of("kernel_file").map(String::from), - initrd_file: matches.value_of("initrd_file").map(String::from), - ovmf_file: matches.value_of("ovmf_file").map(String::from), - cmdline: matches.value_of("cmdline").map(String::from), - vcpus: matches.value_of("vcpus").map(|v| v.parse().unwrap()), - vcpu_type: matches.value_of("vcpu_type").map(String::from), - vmm_type: matches.value_of("vmm_type").map(String::from), - guest_features: matches.value_of("guest_features").map(String::from), - }; - - // Output arguments in a nicely formatted JSON style - let formatted_json = serde_json::to_string_pretty(&args).unwrap(); - println!("{}", formatted_json); - - println!("\n=== Parsing Command Line Arguments ==="); - - // Check if a config file path is provided, and load the configuration - let config: Option = if let Some(config_path) = matches.value_of("config") { - let config_content = fs::read_to_string(config_path) - .map_err(|e| format!("Failed to read config file: {:?}", e)) - .unwrap(); - let config: Config = serde_yaml::from_str(&config_content) - .map_err(|e| format!("Failed to parse config file: {:?}", e)) - .unwrap(); - Some(config) - } else { - None - }; - - // If a config file is loaded, print it as formatted JSON - if let Some(config) = config.as_ref() { - let formatted_json = serde_json::to_string_pretty(&config).unwrap(); - println!("{}", formatted_json); - } else { - println!("No config loaded."); - } - - // Retrieve the kernel file from either the config or the command line arguments - let kernel_file = config - .as_ref() - .and_then(|c| Some(c.kernel_file.clone())) - .unwrap_or_else(|| matches.value_of("kernel_file").unwrap().to_owned()); - - // Process other command line arguments or config values similarly... - let initrd_file = config - .as_ref() - .and_then(|c| Some(c.initrd_file.clone())) - .or_else(|| matches.value_of("initrd_file").map(|s| s.to_owned())); - - let ovmf_file = config - .as_ref() - .and_then(|c| Some(c.ovmf_file.clone())) - .unwrap_or_else(|| matches.value_of("ovmf_file").unwrap().to_owned()); - - let cmdline = config - .as_ref() - .and_then(|c| Some(c.cmdline.clone())) - .unwrap_or_else(|| matches.value_of("cmdline").unwrap().to_owned()); - - let vcpus: u32 = config - .as_ref() - .and_then(|c| c.vcpus) - .unwrap_or_else(|| matches.value_of("vcpus").unwrap().parse().unwrap()); - - let vcpu_type = config - .as_ref() - .and_then(|c| c.vcpu_type.clone()) - .unwrap_or_else(|| matches.value_of("vcpu_type").unwrap().to_owned()); - - // Process virtual CPU type - let vcpu_type = match vcpu_type.as_str() { - "Epyc" => CpuType::Epyc, - "EpycV1" => CpuType::EpycV1, - "EpycV2" => CpuType::EpycV2, - "EpycIBPB" => CpuType::EpycIBPB, - "EpycV3" => CpuType::EpycV3, - "EpycV4" => CpuType::EpycV4, - "EpycRome" => CpuType::EpycRome, - "EpycRomeV1" => CpuType::EpycRomeV1, - "EpycRomeV2" => CpuType::EpycRomeV2, - "EpycRomeV3" => CpuType::EpycRomeV3, - "EpycMilan" => CpuType::EpycMilan, - "EpycMilanV1" => CpuType::EpycMilanV1, - "EpycMilanV2" => CpuType::EpycMilanV2, - "EpycGenoa" => CpuType::EpycGenoa, - "EpycGenoaV1" => CpuType::EpycGenoaV1, - _ => CpuType::EpycV4, // Default to EpycV4 - }; - - let vmm_type = config - .as_ref() - .and_then(|c| c.vmm_type.clone()) - .unwrap_or_else(|| matches.value_of("vmm_type").unwrap().to_owned()); - - // Resolve the VMM type - let vmm_type = match vmm_type.as_str() { - "QEMU" => Some(VMMType::QEMU), - "EC2" => Some(VMMType::EC2), - "KRUN" => Some(VMMType::KRUN), - _ => Some(VMMType::QEMU), // Default to QEMU - }; - - let guest_features_string = config - .as_ref() - .and_then(|c| c.guest_features.clone()) - .unwrap_or_else(|| matches.value_of("guest_features").unwrap().to_owned()); - - let guest_features: u64 = - u64::from_str_radix(&guest_features_string, 2).unwrap(); - - - // Step 1: Get the hash of the OVMF file - let ovmf_hash = get_ovmf_hash_from_file(ovmf_file.clone().into()).unwrap(); - let ovmf_bytes: Vec = bincode::serialize(&ovmf_hash).unwrap(); - let ovmf_binding = ovmf_hash.get_hex_ld(); - - println!("\n===== OVFM ====="); - println!("Bytes: {:x?}", ovmf_bytes); - println!("Hash: {:x?}", ovmf_binding); - - // Step 2: Get the hash of the kernel, initrd, and cmdline - let SevHashes { - kernel_hash, - initrd_hash, - cmdline_hash, - } = get_hashes_from_files( - kernel_file.clone().into(), - initrd_file.clone().map(|file| file.into()), - Some(cmdline.as_str()), - ) - .unwrap(); - - println!("\n===== Kernel ====="); - println!("Bytes: {:x?}", kernel_hash); - println!("Hash: {}", bytes_to_hex(&kernel_hash)); - - println!("\n===== Initrd ====="); - println!("Bytes {:x?}", initrd_hash); - println!("Hash: {}", bytes_to_hex(&initrd_hash)); - - - println!("\n===== Cmdline ====="); - println!("Bytes: {:x?}", cmdline_hash); - println!("Hash: {}", bytes_to_hex(&cmdline_hash)); - - // Step 3: Calculate the launch digest - let arguments = SnpMeasurementArgs { - ovmf_file: Some(PathBuf::from(ovmf_file)), - kernel_file: None, - initrd_file: None, - append: None, - - vcpus, - vcpu_type, - vmm_type, - guest_features: GuestFeatures(guest_features), - - ovmf_hash_str: Some(ovmf_binding.as_str()), - kernel_hash: Some(kernel_hash), - initrd_hash: Some(initrd_hash), - append_hash: Some(cmdline_hash), - }; - - let expected_hash = calculate_launch_measurment(arguments).unwrap(); - - println!("\n===== Expected ====="); - println!("Bytes: {:x?}", expected_hash); - println!("Hash: {}", bytes_to_hex(&expected_hash)); -} diff --git a/native/hb_nif/hb_nif.c b/native/hb_nif/hb_nif.c new file mode 100644 index 000000000..6c4647f06 --- /dev/null +++ b/native/hb_nif/hb_nif.c @@ -0,0 +1,35 @@ +#include "hb_nif.h" +#include + +// Utility functions. +// Based on Arweave's c_src/ar_nif.c + +ERL_NIF_TERM solution_tuple(ErlNifEnv* envPtr, ERL_NIF_TERM hashTerm) { + return enif_make_tuple2(envPtr, enif_make_atom(envPtr, "true"), hashTerm); +} + +ERL_NIF_TERM ok_tuple(ErlNifEnv* envPtr, ERL_NIF_TERM term) +{ + return enif_make_tuple2(envPtr, enif_make_atom(envPtr, "ok"), term); +} + +ERL_NIF_TERM ok_tuple2(ErlNifEnv* envPtr, ERL_NIF_TERM term1, ERL_NIF_TERM term2) +{ + return enif_make_tuple3(envPtr, enif_make_atom(envPtr, "ok"), term1, term2); +} + +ERL_NIF_TERM error_tuple(ErlNifEnv* envPtr, const char* reason) +{ + ERL_NIF_TERM reasonTerm = enif_make_string(envPtr, reason, ERL_NIF_LATIN1); + return enif_make_tuple2(envPtr, enif_make_atom(envPtr, "error"), reasonTerm); +} + +ERL_NIF_TERM make_output_binary(ErlNifEnv* envPtr, unsigned char *dataPtr, size_t size) +{ + ERL_NIF_TERM outputTerm; + unsigned char *outputTermDataPtr; + + outputTermDataPtr = enif_make_new_binary(envPtr, size, &outputTerm); + memcpy(outputTermDataPtr, dataPtr, size); + return outputTerm; +} diff --git a/native/hb_nif/hb_nif.h b/native/hb_nif/hb_nif.h new file mode 100644 index 000000000..5f72bb6e8 --- /dev/null +++ b/native/hb_nif/hb_nif.h @@ -0,0 +1,14 @@ +#ifndef HB_NIF_H +#define HB_NIF_H + +#include + +// Based on Arweave's c_src/ar_nif.h + +ERL_NIF_TERM solution_tuple(ErlNifEnv*, ERL_NIF_TERM); +ERL_NIF_TERM ok_tuple(ErlNifEnv*, ERL_NIF_TERM); +ERL_NIF_TERM ok_tuple2(ErlNifEnv*, ERL_NIF_TERM, ERL_NIF_TERM); +ERL_NIF_TERM error_tuple(ErlNifEnv*, const char*); +ERL_NIF_TERM make_output_binary(ErlNifEnv*, unsigned char*, size_t); + +#endif // HB_NIF_H diff --git a/native/lib/Makefile b/native/lib/Makefile new file mode 100644 index 000000000..6b5b3bc88 --- /dev/null +++ b/native/lib/Makefile @@ -0,0 +1,59 @@ +###################################################################### +# HyperBEAM Library GNU Makefile +# +# Usage: make [all|clean] +# Usage: make [secp256k1|secp256k1-clean] +# +# Based on Arweave's lib/Makefile +###################################################################### +SECP256K1_CMAKE_OPTIONS ?= \ + -DSECP256K1_DISABLE_SHARED=ON \ + -DSECP256K1_ENABLE_MODULE_RECOVERY=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DSECP256K1_BUILD_BENCHMARK=OFF \ + -DSECP256K1_BUILD_EXHAUSTIVE_TESTS=OFF \ + -DSECP256K1_BUILD_TESTS=OFF \ + -DSECP256K1_ENABLE_MODULE_MUSIG=OFF \ + -DSECP256K1_ENABLE_MODULE_EXTRAKEYS=OFF \ + -DSECP256K1_ENABLE_MODULE_ELLSWIFT=OFF \ + -DSECP256K1_ENABLE_MODULE_SCHNORRSIG=OFF \ + -DSECP256K1_APPEND_CFLAGS=-fPIC + +GIT_SUBMODULE_OPTIONS ?= --checkout --init + +PHONY += all +all: secp256k1 + +PHONY += help +help: + @echo "Usage: make [all|clean]" + @echo "Usage: make [secp256k1|secp256k1-clean]" + +PHONY += clean +clean: secp256k1-clean + +###################################################################### +# secp256k1 targets +###################################################################### +PHONY += secp256k1 +secp256k1: secp256k1/CMakeLists.txt secp256k1/build/lib/libsecp256k1.a + +PHONY += secp256k1-clean +secp256k1-clean: + -rm -rf secp256k1/build + +secp256k1/CMakeLists.txt: + git submodule update $(GIT_SUBMODULE_OPTIONS) secp256k1 + +secp256k1/build: secp256k1/CMakeLists.txt + mkdir $@ + +secp256k1/build/Makefile: secp256k1/build + cd secp256k1/build \ + && cmake $(SECP256K1_CMAKE_OPTIONS) .. + +secp256k1/build/lib/libsecp256k1.a: secp256k1/build/Makefile + cd secp256k1/build \ + && cmake --build . + +.PHONY: $(PHONY) diff --git a/native/lib/secp256k1 b/native/lib/secp256k1 new file mode 160000 index 000000000..1d146ac3e --- /dev/null +++ b/native/lib/secp256k1 @@ -0,0 +1 @@ +Subproject commit 1d146ac3edd47a6ea10669a18cae62171a8e35c6 diff --git a/native/secp256k1/Makefile b/native/secp256k1/Makefile new file mode 100644 index 000000000..00bf5fb39 --- /dev/null +++ b/native/secp256k1/Makefile @@ -0,0 +1,137 @@ +# secp256k1 NIF Makefile for HyperBEAM +# Based on Arweave's c_src/Makefile + +CURDIR := $(shell pwd) +BASEDIR := $(abspath $(CURDIR)/../..) + +PROJECT ?= $(notdir $(BASEDIR)) +PROJECT := $(strip $(PROJECT)) + +ifeq ($(MODE), debug) + CFLAGS ?= -O0 -g + CXXFLAGS ?= -O0 -g +else + CFLAGS ?= -O3 + CXXFLAGS ?= -O3 +endif + +UNAME_SYS := $(shell uname -s) + +# Set default libs path for secp256k1 implementation +SECP256K1_LDLIBS = -L /usr/lib -L /usr/local/lib + +ifeq ($(UNAME_SYS), Linux) + # _mm_crc32_u32 support + CFLAGS += -msse4.2 + CXXFLAGS += -msse4.2 +endif + +ERTS_INCLUDE_DIR ?= $(shell erl -noshell -eval 'io:format("~ts/erts-~ts/include/", [code:root_dir(), erlang:system_info(version)]).' -s init stop) +ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -eval 'io:format("~ts", [code:lib_dir(erl_interface, include)]).' -s init stop) +ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -eval 'io:format("~ts", [code:lib_dir(erl_interface, lib)]).' -s init stop) + +# System type and C compiler/flags. + +ifeq ($(UNAME_SYS), Darwin) + OSX_CPU_ARCH ?= x86_64 + # nix systems may not have sysctl where uname -m will return the correct arch + SYSCTL_EXISTS := $(shell which sysctl 2>/dev/null) + ifneq ($(shell uname -m | egrep "arm64"),) + OSX_CPU_ARCH = arm64 + else + ifdef SYSCTL_EXISTS + ifneq ($(shell sysctl -n machdep.cpu.brand_string | egrep "M(1|2)"),) + OSX_CPU_ARCH = arm64 + endif + endif + endif + CC ?= cc + CFLAGS += -std=c99 -arch $(OSX_CPU_ARCH) -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS += -arch $(OSX_CPU_ARCH) -finline-functions -Wall + LDFLAGS ?= -arch $(OSX_CPU_ARCH) + LDFLAGS += -undefined suppress + # on MacOS, some libs are also present in /opt/homebrew/lib + SECP256K1_LDLIBS += -L /opt/homebrew/lib +else ifeq ($(UNAME_SYS), FreeBSD) + CC ?= cc + CFLAGS += -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS += -finline-functions -Wall +else ifeq ($(UNAME_SYS), Linux) + CC ?= gcc + CFLAGS += -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS += -finline-functions -Wall +endif + +C_SRC_DIR = $(CURDIR) + +# Paths to libsecp256k1 +LIBSECP256K1_DIR = $(BASEDIR)/native/lib/secp256k1 +LIBSECP256K1_STATIC = $(LIBSECP256K1_DIR)/build/lib/libsecp256k1.a + +# Source files +SECP256K1_SOURCES = $(C_SRC_DIR)/secp256k1_nif.c $(BASEDIR)/native/hb_nif/hb_nif.c +SECP256K1_OBJECTS = $(C_SRC_DIR)/secp256k1_nif.o $(BASEDIR)/native/hb_nif/hb_nif.o + +# Build up SECP256K1 flags (matching Arweave's pattern) +SECP256K1_CFLAGS += $(CFLAGS) +SECP256K1_LDLIBS += $(LDFLAGS) +CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I /usr/local/include -I $(C_SRC_DIR) +CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -std=c++11 +LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -L /usr/local/lib -lei + +# Final secp256k1-specific flags +SECP256K1_CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I /usr/local/include -I $(LIBSECP256K1_DIR)/include -I $(BASEDIR)/native/hb_nif -I $(C_SRC_DIR) +SECP256K1_LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) + +# Output +SECP256K1_OUTPUT ?= $(BASEDIR)/priv/secp256k1_arweave.so + +# Verbosity. + +c_verbose_0 = @echo " C " $(?F); +c_verbose = $(c_verbose_$(V)) + +cpp_verbose_0 = @echo " CPP " $(?F); +cpp_verbose = $(cpp_verbose_$(V)) + +link_verbose_0 = @echo " LD " $(@F); +link_verbose = $(link_verbose_$(V)) + +COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c +COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c + +$(SECP256K1_OUTPUT): $(LIBSECP256K1_STATIC) $(SECP256K1_OBJECTS) + @mkdir -p $(BASEDIR)/priv/ +ifeq ($(UNAME_SYS), Darwin) + $(link_verbose) $(CC) $(SECP256K1_OBJECTS) $(LIBSECP256K1_STATIC) -bundle -flat_namespace -undefined suppress $(SECP256K1_LDLIBS) -o $(SECP256K1_OUTPUT) +else + $(link_verbose) $(CC) $(SECP256K1_OBJECTS) $(LIBSECP256K1_STATIC) -shared $(SECP256K1_LDLIBS) -o $(SECP256K1_OUTPUT) +endif + +%secp256k1_nif.o: %secp256k1_nif.c + $(c_verbose) $(CC) $(SECP256K1_CFLAGS) -c $(OUTPUT_OPTION) $< + +%hb_nif.o: %hb_nif.c + $(c_verbose) $(CC) $(SECP256K1_CFLAGS) -c $(OUTPUT_OPTION) $< + +%.o: %.c + $(COMPILE_C) $(OUTPUT_OPTION) $< + +%.o: %.cc + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.C + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.cpp + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +all: $(SECP256K1_OUTPUT) + +$(LIBSECP256K1_STATIC): + $(MAKE) -C $(BASEDIR)/native/lib secp256k1 + +clean: + @rm -f $(SECP256K1_OUTPUT) $(SECP256K1_OBJECTS) + +.PHONY: all clean diff --git a/native/secp256k1/secp256k1_nif.c b/native/secp256k1/secp256k1_nif.c new file mode 100644 index 000000000..91c9e2535 --- /dev/null +++ b/native/secp256k1/secp256k1_nif.c @@ -0,0 +1,272 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +// Based on Arweave's c_src/secp256k1/secp256k1_nif.c + +#define SECP256K1_PUBKEY_UNCOMPRESSED_SIZE 65 +#define SECP256K1_PUBKEY_COMPRESSED_SIZE 33 +#define SECP256K1_SIGNATURE_COMPACT_SIZE 64 +#define SECP256K1_SIGNATURE_RECOVERABLE_SIZE 65 +#define SECP256K1_PRIVKEY_SIZE 32 +#define SECP256K1_CONTEXT_SEED_SIZE 32 +#define SECP256K1_DIGEST_SIZE 32 + +static int secp256k1_load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info) { + return 0; +} + +static int fill_devurandom(void* buffer, size_t size) { + int fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC); + if (fd == -1) { + return 0; + } + + size_t offset = 0; + while (offset < size) { + ssize_t result = read(fd, (char*)buffer + offset, size - offset); + if (result == -1) { + if (errno == EINTR) continue; + goto error; + } + // EOF + if (result == 0) { + goto error; + } + offset += (size_t)result; + } + + close(fd); + return 1; + +error: + close(fd); + return 0; +} + +static int fill_random(void* buffer, size_t size) { +#if defined(__linux__) || defined(__FreeBSD__) + + size_t offset = 0; + while (offset < size) { + ssize_t result = getrandom((char*)buffer + offset, size - offset, 0); + if (result == -1) { + if (errno == EINTR) continue; + if (errno == ENOSYS) return fill_devurandom(buffer, size); + return 0; + } + offset += (size_t)result; + } + +#elif defined(__APPLE__) + + size_t offset = 0; + while (offset < size) { + // max allowed length is 256 bytes + size_t chunk = (size - offset > 256) ? 256 : (size - offset); + if (getentropy((char*)buffer + offset, chunk) == -1) { + if (errno == ENOSYS) return fill_devurandom(buffer, size); + return 0; + } + offset += chunk; + } + +#else + // Unsupported platform + return 0; +#endif + return 1; +} + +/* Cleanses memory to prevent leaking sensitive info. Won't be optimized out. */ +static void secure_erase(void *ptr, size_t len) { +#if defined(__GNUC__) + /* We use a memory barrier that scares the compiler away from optimizing out the memset. + * + * Quoting Adam Langley in commit ad1907fe73334d6c696c8539646c21b11178f20f + * in BoringSSL (ISC License): + * As best as we can tell, this is sufficient to break any optimisations that + * might try to eliminate "superfluous" memsets. + * This method used in memzero_explicit() the Linux kernel, too. Its advantage is that it is + * pretty efficient, because the compiler can still implement the memset() efficiently, + * just not remove it entirely. See "Dead Store Elimination (Still) Considered Harmful" by + * Yang et al. (USENIX Security 2017) for more background. + */ + memset(ptr, 0, len); + __asm__ __volatile__("" : : "r"(ptr) : "memory"); +#else + void *(*volatile const volatile_memset)(void *, int, size_t) = memset; + volatile_memset(ptr, 0, len); +#endif +} + +static ERL_NIF_TERM sign_recoverable(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 2) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, PrivateBytes; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &PrivateBytes)) { + return enif_make_badarg(env); + } + if (PrivateBytes.size != SECP256K1_PRIVKEY_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char seed[SECP256K1_CONTEXT_SEED_SIZE]; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char privbytes[SECP256K1_PRIVKEY_SIZE]; + unsigned char signature_compact[SECP256K1_SIGNATURE_COMPACT_SIZE]; + unsigned char signature_recoverable[SECP256K1_SIGNATURE_RECOVERABLE_SIZE]; + int recid; + secp256k1_ecdsa_recoverable_signature s; + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE); + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(privbytes, PrivateBytes.data, SECP256K1_PRIVKEY_SIZE); + + if (!secp256k1_ec_seckey_verify(ctx, privbytes)) { + error = "secp256k1 key is invalid."; + goto cleanup; + } + + if (!fill_random(seed, sizeof(seed))) { + error = "Failed to generate random seed for context."; + goto cleanup; + } + + if (!secp256k1_context_randomize(ctx, seed)) { + error = "Failed to randomize context."; + goto cleanup; + } + + if(!secp256k1_ecdsa_sign_recoverable(ctx, &s, digest, privbytes, NULL, NULL)) { + error = "Failed to create signature."; + goto cleanup; + } + + if(!secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, signature_compact, &recid, &s)) { + error = "Failed to serialize signature."; + goto cleanup; + } + memcpy(signature_recoverable, signature_compact, SECP256K1_SIGNATURE_COMPACT_SIZE); + signature_recoverable[64] = (unsigned char)(recid); + + ERL_NIF_TERM signature_term = make_output_binary(env, signature_recoverable, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + +cleanup: + secp256k1_context_destroy(ctx); + secure_erase(seed, sizeof(seed)); + secure_erase(privbytes, sizeof(privbytes)); + memset(signature_compact, 0, SECP256K1_SIGNATURE_COMPACT_SIZE); + memset(signature_recoverable, 0, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + if (error) { + return error_tuple(env, error); + } + return ok_tuple(env, signature_term); +} + +static ERL_NIF_TERM recover_pk_and_verify(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + if (argc != 2) { + return enif_make_badarg(env); + } + ErlNifBinary Digest, Signature; + if (!enif_inspect_binary(env, argv[0], &Digest)) { + return enif_make_badarg(env); + } + if (Digest.size != SECP256K1_DIGEST_SIZE) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[1], &Signature)) { + return enif_make_badarg(env); + } + if (Signature.size != SECP256K1_SIGNATURE_RECOVERABLE_SIZE) { + return enif_make_badarg(env); + } + + char *error = NULL; + unsigned char digest[SECP256K1_DIGEST_SIZE]; + unsigned char signature_recoverable[SECP256K1_SIGNATURE_RECOVERABLE_SIZE]; + unsigned char signature_compact[SECP256K1_SIGNATURE_COMPACT_SIZE]; + unsigned char pubbytes[SECP256K1_PUBKEY_COMPRESSED_SIZE]; + int recid; + secp256k1_ecdsa_recoverable_signature rs; + secp256k1_ecdsa_signature s; + secp256k1_pubkey pubkey; + + memcpy(digest, Digest.data, SECP256K1_DIGEST_SIZE); + memcpy(signature_recoverable, Signature.data, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + memcpy(signature_compact, signature_recoverable, SECP256K1_SIGNATURE_COMPACT_SIZE); + recid = (int)signature_recoverable[64]; + + if (recid < 0 || recid > 3) { + error = "Invalid signature recid. recid >= 0 && recid <= 3."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recoverable_signature_parse_compact(secp256k1_context_static, &rs, signature_compact, recid)) { + error = "Failed to deserialize/parse recoverable signature."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recover(secp256k1_context_static, &pubkey, &rs, digest)) { + error = "Failed to recover public key."; + goto cleanup; + } + size_t l = SECP256K1_PUBKEY_COMPRESSED_SIZE; + if (!secp256k1_ec_pubkey_serialize(secp256k1_context_static, pubbytes, &l, &pubkey, SECP256K1_EC_COMPRESSED)) { + error = "Failed to serialize the recovered public key."; + goto cleanup; + } + + if (!secp256k1_ecdsa_recoverable_signature_convert(secp256k1_context_static, &s, &rs)) { + error = "Failed to convert recoverable signature to compact signature."; + goto cleanup; + } + + // NOTE. https://github.com/bitcoin-core/secp256k1/blob/f79f46c70386c693ff4e7aef0b9e7923ba284e56/src/secp256k1.c#L461 + // Verify performs check for low-s + int is_valid = secp256k1_ecdsa_verify(secp256k1_context_static, &s, digest, &pubkey); + ERL_NIF_TERM pubkey_term = make_output_binary(env, pubbytes, SECP256K1_PUBKEY_COMPRESSED_SIZE); + +cleanup: + memset(digest, 0, SECP256K1_DIGEST_SIZE); + memset(pubbytes, 0, SECP256K1_PUBKEY_COMPRESSED_SIZE); + memset(signature_compact, 0, SECP256K1_SIGNATURE_COMPACT_SIZE); + memset(signature_recoverable, 0, SECP256K1_SIGNATURE_RECOVERABLE_SIZE); + + if (error) { + return error_tuple(env, error); + } + if (is_valid) { + return ok_tuple2(env, enif_make_atom(env, "true"), pubkey_term); + } + return ok_tuple2(env, enif_make_atom(env, "false"), pubkey_term); +} + +static ErlNifFunc nif_funcs[] = { + {"sign_recoverable", 2, sign_recoverable}, + {"recover_pk_and_verify", 2, recover_pk_and_verify} +}; + +ERL_NIF_INIT(secp256k1_nif, nif_funcs, secp256k1_load, NULL, NULL, NULL) diff --git a/rebar.config b/rebar.config index 9127ae03f..83c8910f5 100644 --- a/rebar.config +++ b/rebar.config @@ -1,11 +1,82 @@ -{erl_opts, [debug_info, {d, 'COWBOY_QUICER', 1}, {d, 'GUN_QUICER', 1}]}. -{plugins, [pc, rebar3_rustler, rebar_edown_plugin]}. +{erl_opts, [ + debug_info, + {d, 'COWBOY_QUICER', 1}, + {d, 'GUN_QUICER', 1}, + {i, "src/core"} +]}. + +%% The repo is split into three top-level children under `src': +%% - `src/core' AO-Core runtime, stores, HTTP, Arweave, helpers +%% - `src/forge' device packager, rebar3 providers, tests +%% - `src/preloaded' Erlang sources for the devices baked into the +%% build by the Forge preload pipeline +%% Source files are no longer present at the bare `src/' root, except +%% for `hb.app.src' which rebar3 discovers there before applying the +%% `src_dirs' setting. +{project_app_dirs, ["."]}. +{src_dirs, [ + {"src", [{recursive, false}]}, + {"src/core", [{recursive, true}]}, + {"src/forge", [{recursive, false}]} +]}. +{checkouts_dir, "src/forge"}. + +% `hb_test_parallel' is used as a parse_transform by every module that +% includes `hb.hrl' under `-ifdef(TEST)', so it must be compiled first. +{erl_first_files, [ + "src/core/test/hb_test_parallel.erl" +]}. + +{plugins, [pc, rebar_edown_plugin, {rebar3_eunit_start, {git, "https://github.com/permaweb/rebar3_eunit_start.git", {ref, "04ec53fea187039770db0d4459b7aeb01a9021af"}}}]}. +{project_plugins, [plugin]}. + +% Increase `scale_timeouts` when running on a slower machine. +{eunit_opts, [verbose, {scale_timeouts, 10}, {start_applications, [prometheus, hb]}]}. {profiles, [ + {quiet, + [ + {eunit_opts, [{verbose, false}, {scale_timeouts, 10}]}, + {erl_opts, [{d, 'QUIET', true}]} + ] + }, + {no_events, [{erl_opts, [{d, 'NO_EVENTS', true}]}]}, + {top, [ + {deps, [{observer_cli, {git, "https://github.com/permaweb/observer_cli.git", + {ref, "7e7a2613b262e08c43c539d50901e6f26a241b6f"}}}]}, + {erl_opts, [{d, 'AO_TOP', true}]}, + {relx, [{release, {'hb', "0.0.1"}, [hb, b64rs, base32, cowboy, gun, luerl, prometheus, prometheus_cowboy, prometheus_ranch, elmdb, observer_cli, runtime_tools]}]} + ]}, + {store_events, [{erl_opts, [{d, 'STORE_EVENTS', true}]}]}, + {ao_profiling, [{erl_opts, [{d, 'AO_PROFILING', true}]}]}, + {eflame, + [ + {deps, + [ + {eflame, + {git, + "https://github.com/samcamwilliams/eflame.git", + {ref, "d81a6e174956b4b0aca13363d51e4f51a5fabbd2"} + } + } + ] + }, + {erl_opts, [{d, 'ENABLE_EFLAME', true}]} + ] + }, {genesis_wasm, [ {erl_opts, [{d, 'ENABLE_GENESIS_WASM', true}]}, {pre_hooks, [ - {compile, "make -C \"${REBAR_ROOT_DIR}\" setup-genesis-wasm"} + {compile, "make -C . setup-genesis-wasm"} + ]}, + {relx, [ + {overlay, [ + {copy, + "_build/genesis_wasm/genesis-wasm-server", + "genesis-wasm-server" + } + ]}, + {sys_config, "config/app.config"} ]} ]}, {rocksdb, [ @@ -32,43 +103,60 @@ ]} ]}. -{cargo_opts, [ - {src_dir, "native/dev_snp_nif"} +{overrides, [ + {override, gun, [{deps, [cowlib]}]} ]}. -{overrides, []}. - {pre_hooks, [ - {compile, "bash -c \"echo '-define(HB_BUILD_SOURCE, <<\\\"$(git rev-parse HEAD)\\\">>).\n' > ${REBAR_ROOT_DIR}/_build/hb_buildinfo.hrl\""}, - {compile, "bash -c \"echo '-define(HB_BUILD_SOURCE_SHORT, <<\\\"$(git rev-parse --short HEAD)\\\">>).\n' >> ${REBAR_ROOT_DIR}/_build/hb_buildinfo.hrl\""}, - {compile, "bash -c \"echo '-define(HB_BUILD_TIME, $(date +%s)).\n' >> ${REBAR_ROOT_DIR}/_build/hb_buildinfo.hrl\""}, - {compile, "make -C \"${REBAR_ROOT_DIR}\" wamr"} + {compile, "bash -c \"mkdir -p _build && printf '%s\\n\\n%s\\n\\n%s\\n' '-define(HB_BUILD_SOURCE, <<\\\"$(git rev-parse HEAD)\\\">>).' '-define(HB_BUILD_SOURCE_SHORT, <<\\\"$(git rev-parse --short HEAD)\\\">>).' '-define(HB_BUILD_TIME, $(date +%s)).' > _build/hb_buildinfo.hrl\""}, + {compile, "bash -c \"mkdir -p _build && (test -f _build/hb_preloaded_index.hrl || printf '%s\\n\\n%s\\n' '%% Generated by hb_preload - do not edit.' '-define(PRELOADED_DEVICES_INDEX_MESSAGE_ID, undefined).' > _build/hb_preloaded_index.hrl)\""}, + {compile, "make -C . wamr"}, + {"(linux|darwin)", compile, "make -C native/lib all"}, + {"(linux|darwin)", compile, "make -C native/secp256k1 all"} ]}. {port_env, [ {"(linux|darwin|solaris)", "CFLAGS", - "$CFLAGS -I${REBAR_ROOT_DIR}/_build/wamr/core/iwasm/include -I/usr/local/lib/erlang/usr/include/"}, - {"(linux|darwin|solaris)", "LDFLAGS", "$LDFLAGS -L${REBAR_ROOT_DIR}/_build/wamr/lib -lvmlib -lei"}, + "$CFLAGS -I_build/wamr/core/iwasm/include -I/usr/local/lib/erlang/usr/include/"}, + {"(linux|darwin|solaris)", "LDFLAGS", "$LDFLAGS -L_build/wamr/lib -lvmlib -lei"}, {"(linux|darwin|solaris)", "LDLIBS", "-lei"} ]}. {post_hooks, [ - {"(linux|darwin|solaris)", clean, "rm -rf \"${REBAR_ROOT_DIR}/_build\" \"${REBAR_ROOT_DIR}/priv\""}, + {"(linux|darwin)", clean, "make -C native/secp256k1 clean"}, + {"(linux|darwin)", clean, "make -C native/lib secp256k1-clean"}, + {"(linux|darwin|solaris)", clean, "rm -rf _build priv"}, {"(linux|darwin|solaris)", compile, "echo 'Post-compile hooks executed'"}, - { compile, "rm -f native/hb_beamr/*.o native/hb_beamr/*.d"}, - { compile, "rm -f native/hb_keccak/*.o native/hb_keccak/*.d"}, - { compile, "mkdir -p priv/html"}, - { compile, "cp -R src/html/* priv/html"} + { compile, + "sh -c '" + "rm -f native/hb_beamr/*.o native/hb_beamr/*.d; " + "rm -f native/hb_keccak/*.o native/hb_keccak/*.d; " + "mkdir -p priv/html; " + "cp -R src/core/html/* priv/html; " + "cp _build/default/lib/elmdb/priv/crates/elmdb_nif/elmdb_nif.so " + "_build/default/lib/elmdb/priv/elmdb_nif.so 2>/dev/null || true" + "'" + }, + %% Run the Forge preloader over `src/preloaded' so every build + %% finishes with a `_build/preloaded-store' and a matching + %% `_build/hb_preloaded_index.hrl' header for the core default + %% config to consume. + {"(linux|darwin|solaris)", compile, + "sh -c '" + "case \"$PWD\" in " + "*/_build/*) ;; " + "*) escript scripts/build-preloaded-store.escript ;; " + "esac" + "'"} ]}. {provider_hooks, [ {pre, [ - {compile, {cargo, build}} + {eunit, {default, rebar3_eunit_start}} ]}, {post, [ {compile, {pc, compile}}, - {clean, {pc, clean}}, - {clean, {cargo, clean}} + {clean, {pc, clean}} ]} ]}. @@ -84,41 +172,55 @@ "./native/hb_keccak/hb_keccak.c", "./native/hb_keccak/hb_keccak_nif.c" ]} + %% secp256k1_arweave.so is built via native/secp256k1/Makefile ]}. {deps, [ - {b64fast, {git, "https://github.com/ArweaveTeam/b64fast.git", {ref, "58f0502e49bf73b29d95c6d02460d1fb8d2a5273"}}}, - {cowboy, {git, "https://github.com/ninenines/cowboy", {ref, "022013b6c4e967957c7e0e7e7cdefa107fc48741"}}}, - {gun, {git, "https://github.com/ninenines/gun", {ref, "8efcedd3a089e6ab5317e4310fed424a4ee130f8"}}}, - {prometheus, "4.11.0"}, - {prometheus_cowboy, "0.1.8"}, - {gun, "0.10.0"}, - {luerl, "1.3.0"} + {elmdb, {git, "https://github.com/permaweb/elmdb-rs.git", {ref, "06ccf937abc250cb22c782d568efcaa39f5452ff"}}}, + {b64rs, {git, "https://github.com/permaweb/b64rs.git", {ref, "94b7d8e51d9a44f3bd12b7d138dd0d2cb74c169f"}}}, + {base32, "1.0.0"}, + {cowlib, "2.16.0"}, + {cowboy, "2.14.0"}, + {ranch, "2.2.0"}, + {gun, "2.2.0"}, + {prometheus_cowboy, "0.2.0"}, + {prometheus_ranch, {git, "https://github.com/permaweb/prometheus_ranch.git", {ref, "73f16ed9856972ced3fb8f4168004fffe742d5b2"}}}, + {prometheus_httpd, "2.1.15"}, + {prometheus, "6.0.3"}, + {graphql, "0.17.1", {pkg, graphql_erl}}, + {luerl, "1.3.0"}, + {hackney, "1.25.0"}, + {eqwalizer_support, + {git_subdir, + "https://github.com/whatsapp/eqwalizer.git", + {branch, "main"}, + "eqwalizer_support"}} ]}. {shell, [ - {apps, [hb]} + {apps, [hb]}, + {config, "config/app.config"} ]}. {eunit, [ {apps, [hb]} ]}. -{eunit_opts, [verbose]}. - {relx, [ - {release, {'hb', "0.0.1"}, [hb, b64fast, cowboy, gun, luerl, prometheus, prometheus_cowboy]}, + {release, {'hb', "0.0.1"}, [hb, b64rs, base32, cowboy, gun, luerl, prometheus, prometheus_cowboy, prometheus_ranch, elmdb, runtime_tools]}, + {sys_config, "config/app.config"}, {include_erts, true}, {extended_start_script, true}, {overlay, [ {mkdir, "bin/priv"}, {copy, "priv", "bin/priv"}, - {copy, "config.flat", "config.flat"} + {copy, "config.flat", "config.flat"}, + {copy, "_build/preloaded-store", "_build/preloaded-store"} ]} ]}. {dialyzer, [ - {plt_extra_apps, [public_key, ranch, cowboy, prometheus, prometheus_cowboy, b64fast, eunit, gun]}, + {plt_extra_apps, [public_key, ranch, cowboy, prometheus, prometheus_cowboy, prometheus_ranch, b64rs, eunit, gun]}, incremental, {warnings, [no_improper_lists, no_unused]} ]}. @@ -133,6 +235,16 @@ [ {eunit, "--module dev_lua_test"} ] + }, + {'eunit-all', + [ + {device, "test --with-core"} + ] + }, + {'deploy-scripts', + [ + {shell, "--eval hb:deploy_scripts()."} + ] } ]}. @@ -143,4 +255,4 @@ {preprocess, true}, {private, true}, {hidden, true} -]}. \ No newline at end of file +]}. diff --git a/rebar.lock b/rebar.lock index a6c3e3287..479c45556 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,43 +1,82 @@ {"1.2.0", -[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.5">>},2}, - {<<"b64fast">>, - {git,"https://github.com/ArweaveTeam/b64fast.git", - {ref,"58f0502e49bf73b29d95c6d02460d1fb8d2a5273"}}, +[{<<"accept">>,{pkg,<<"accept">>,<<"0.3.7">>},1}, + {<<"b64rs">>, + {git,"https://github.com/permaweb/b64rs.git", + {ref,"94b7d8e51d9a44f3bd12b7d138dd0d2cb74c169f"}}, 0}, - {<<"cowboy">>, - {git,"https://github.com/ninenines/cowboy", - {ref,"022013b6c4e967957c7e0e7e7cdefa107fc48741"}}, + {<<"base32">>,{pkg,<<"base32">>,<<"1.0.0">>},0}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"2.15.0">>},1}, + {<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.14.0">>},0}, + {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.16.0">>},0}, + {<<"ddskerl">>,{pkg,<<"ddskerl">>,<<"0.4.2">>},1}, + {<<"elmdb">>, + {git,"https://github.com/permaweb/elmdb-rs.git", + {ref,"06ccf937abc250cb22c782d568efcaa39f5452ff"}}, 0}, - {<<"cowlib">>, - {git,"https://github.com/ninenines/cowlib", - {ref,"1c3d5defba28e92a88ce45c440d57e178ab1c514"}}, - 1}, - {<<"gun">>, - {git,"https://github.com/ninenines/gun", - {ref,"8efcedd3a089e6ab5317e4310fed424a4ee130f8"}}, + {<<"eqwalizer_support">>, + {git_subdir,"https://github.com/whatsapp/eqwalizer.git", + {ref,"0f514eb3893fa7070835c83ecb49fbea31b0426d"}, + "eqwalizer_support"}, 0}, + {<<"graphql">>,{pkg,<<"graphql_erl">>,<<"0.17.1">>},0}, + {<<"gun">>,{pkg,<<"gun">>,<<"2.2.0">>},0}, + {<<"hackney">>,{pkg,<<"hackney">>,<<"1.25.0">>},0}, + {<<"idna">>,{pkg,<<"idna">>,<<"6.1.1">>},1}, {<<"luerl">>,{pkg,<<"luerl">>,<<"1.3.0">>},0}, - {<<"prometheus">>,{pkg,<<"prometheus">>,<<"4.11.0">>},0}, - {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.1.8">>},0}, - {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.11">>},1}, - {<<"quantile_estimator">>,{pkg,<<"quantile_estimator">>,<<"0.2.1">>},1}, - {<<"ranch">>, - {git,"https://github.com/ninenines/ranch", - {ref,"a692f44567034dacf5efcaa24a24183788594eb7"}}, - 1}]}. + {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},1}, + {<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.4.0">>},1}, + {<<"parse_trans">>,{pkg,<<"parse_trans">>,<<"3.4.1">>},1}, + {<<"prometheus">>,{pkg,<<"prometheus">>,<<"6.0.3">>},0}, + {<<"prometheus_cowboy">>,{pkg,<<"prometheus_cowboy">>,<<"0.2.0">>},0}, + {<<"prometheus_httpd">>,{pkg,<<"prometheus_httpd">>,<<"2.1.15">>},0}, + {<<"prometheus_ranch">>, + {git,"https://github.com/permaweb/prometheus_ranch.git", + {ref,"73f16ed9856972ced3fb8f4168004fffe742d5b2"}}, + 0}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"2.2.0">>},0}, + {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},1}, + {<<"unicode_util_compat">>,{pkg,<<"unicode_util_compat">>,<<"0.7.1">>},1}]}. [ {pkg_hash,[ - {<<"accept">>, <<"B33B127ABCA7CC948BBE6CAA4C263369ABF1347CFA9D8E699C6D214660F10CD1">>}, + {<<"accept">>, <<"CD6E34A2D7E28CA38B2D3CB233734CA0C221EFBC1F171F91FEC5F162CC2D18DA">>}, + {<<"base32">>, <<"1AB331F812FCC254C8F7D4348E1E5A6F2B9B32B7A260BF2BC3358E3BF14C841A">>}, + {<<"certifi">>, <<"0E6E882FCDAAA0A5A9F2B3DB55B1394DBA07E8D6D9BCAD08318FB604C6839712">>}, + {<<"cowboy">>, <<"565DCF221BA99B1255B0ADCEC24D2D8DBE79E46EC79F30F8373CCEADC6A41E2A">>}, + {<<"cowlib">>, <<"54592074EBBBB92EE4746C8A8846E5605052F29309D3A873468D76CDF932076F">>}, + {<<"ddskerl">>, <<"A51A90BE9AC9B36A94017670BED479C623B10CA9D4BDA1EDF3A0E48CAEEADA2A">>}, + {<<"graphql">>, <<"EB59FCBB39F667DC1C78C950426278015C3423F7A6ED2A121D3DB8B1D2C5F8B4">>}, + {<<"gun">>, <<"B8F6B7D417E277D4C2B0DC3C07DFDF892447B087F1CC1CAFF9C0F556B884E33D">>}, + {<<"hackney">>, <<"390E9B83F31E5B325B9F43B76E1A785CBDB69B5B6CD4E079AA67835DED046867">>}, + {<<"idna">>, <<"8A63070E9F7D0C62EB9D9FCB360A7DE382448200FBBD1B106CC96D3D8099DF8D">>}, {<<"luerl">>, <<"B56423DDB721432AB980B818FEECB84ADBAB115E2E11522CF94BCD0729CAA501">>}, - {<<"prometheus">>, <<"B95F8DE8530F541BD95951E18E355A840003672E5EDA4788C5FA6183406BA29A">>}, - {<<"prometheus_cowboy">>, <<"CFCE0BC7B668C5096639084FCD873826E6220EA714BF60A716F5BD080EF2A99C">>}, - {<<"prometheus_httpd">>, <<"F616ED9B85B536B195D94104063025A91F904A4CFC20255363F49A197D96C896">>}, - {<<"quantile_estimator">>, <<"EF50A361F11B5F26B5F16D0696E46A9E4661756492C981F7B2229EF42FF1CD15">>}]}, + {<<"metrics">>, <<"25F094DEA2CDA98213CECC3AEFF09E940299D950904393B2A29D191C346A8486">>}, + {<<"mimerl">>, <<"3882A5CA67FBBE7117BA8947F27643557ADEC38FA2307490C4C4207624CB213B">>}, + {<<"parse_trans">>, <<"6E6AA8167CB44CC8F39441D05193BE6E6F4E7C2946CB2759F015F8C56B76E5FF">>}, + {<<"prometheus">>, <<"95302236124C0F919163A7762BF7D2B171B919B6FF6148D26EB38A5D2DEF7B81">>}, + {<<"prometheus_cowboy">>, <<"526F75D9850A9125496F78BCEECCA0F237BC7B403C976D44508543AE5967DAD9">>}, + {<<"prometheus_httpd">>, <<"8F767D819A5D36275EAB9264AFF40D87279151646776069BF69FBDBBD562BD75">>}, + {<<"ranch">>, <<"25528F82BC8D7C6152C57666CA99EC716510FE0925CB188172F41CE93117B1B0">>}, + {<<"ssl_verify_fun">>, <<"354C321CF377240C7B8716899E182CE4890C5938111A1296ADD3EC74CF1715DF">>}, + {<<"unicode_util_compat">>, <<"A48703A25C170EEDADCA83B11E88985AF08D35F37C6F664D6DCFB106A97782FC">>}]}, {pkg_hash_ext,[ - {<<"accept">>, <<"11B18C220BCC2EAB63B5470C038EF10EB6783BCB1FCDB11AA4137DEFA5AC1BB8">>}, + {<<"accept">>, <<"CA69388943F5DAD2E7232A5478F16086E3C872F48E32B88B378E1885A59F5649">>}, + {<<"base32">>, <<"0449285348ED0C4CD83C7198E76C5FD5A0451C4EF18695B9FD43792A503E551A">>}, + {<<"certifi">>, <<"B147ED22CE71D72EAFDAD94F055165C1C182F61A2FF49DF28BCC71D1D5B94A60">>}, + {<<"cowboy">>, <<"EA99769574550FE8A83225C752E8A62780A586770EF408816B82B6FE6D46476B">>}, + {<<"cowlib">>, <<"7F478D80D66B747344F0EA7708C187645CFCC08B11AA424632F78E25BF05DB51">>}, + {<<"ddskerl">>, <<"63F907373D7E548151D584D4DA8A38928FD26EC9477B94C0FFAAD87D7CB69FE7">>}, + {<<"graphql">>, <<"4D0F08EC57EF0983E2596763900872B1AB7E94F8EE3817B9F67EEC911FF7C386">>}, + {<<"gun">>, <<"76022700C64287FEB4DF93A1795CFF6741B83FB37415C40C34C38D2A4645261A">>}, + {<<"hackney">>, <<"7209BFD75FD1F42467211FF8F59EA74D6F2A9E81CBCEE95A56711EE79FD6B1D4">>}, + {<<"idna">>, <<"92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA">>}, {<<"luerl">>, <<"6B3138AA829F0FBC4CD0F083F273B4030A2B6CE99155194A6DB8C67B2C3480A4">>}, - {<<"prometheus">>, <<"719862351AABF4DF7079B05DC085D2BBCBE3AC0AC3009E956671B1D5AB88247D">>}, - {<<"prometheus_cowboy">>, <<"BA286BECA9302618418892D37BCD5DC669A6CC001F4EB6D6AF85FF81F3F4F34C">>}, - {<<"prometheus_httpd">>, <<"0BBE831452CFDF9588538EB2F570B26F30C348ADAE5E95A7D87F35A5910BCF92">>}, - {<<"quantile_estimator">>, <<"282A8A323CA2A845C9E6F787D166348F776C1D4A41EDE63046D72D422E3DA946">>}]} + {<<"metrics">>, <<"69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16">>}, + {<<"mimerl">>, <<"13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144">>}, + {<<"parse_trans">>, <<"620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A">>}, + {<<"prometheus">>, <<"53554ECADAC0354066801D514D1A244DD026175E4EE3A9A30192B71D530C8268">>}, + {<<"prometheus_cowboy">>, <<"2C7EB12F4B970D91E3B47BAAD0F138F6ADC34E53EEB0AE18068FF0AFAB441B24">>}, + {<<"prometheus_httpd">>, <<"67736D000745184D5013C58A63E947821AB90CB9320BC2E6AE5D3061C6FFE039">>}, + {<<"ranch">>, <<"FA0B99A1780C80218A4197A59EA8D3BDAE32FBFF7E88527D7D8A4787EFF4F8E7">>}, + {<<"ssl_verify_fun">>, <<"FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8">>}, + {<<"unicode_util_compat">>, <<"B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642">>}]} ]. diff --git a/release/snpguest b/release/snpguest deleted file mode 100755 index cd52f894e..000000000 Binary files a/release/snpguest and /dev/null differ diff --git a/scripts/build-preloaded-store.escript b/scripts/build-preloaded-store.escript new file mode 100755 index 000000000..83a0ccca2 --- /dev/null +++ b/scripts/build-preloaded-store.escript @@ -0,0 +1,100 @@ +#!/usr/bin/env escript + +%%% @doc Build the in-repo `_build/preloaded-store' from `src/preloaded'. +%%% +%%% Invoked from the rebar.config post-compile hook so every build of +%%% HyperBEAM ends with a working `preloaded-store' on disk and a +%%% matching `_build/hb_preloaded_index.hrl' header. The store is signed +%%% with the node wallet, so the runtime's default device-author trust +%%% rule (trust the node wallet unless configured otherwise) applies. + +-include("../include/hb.hrl"). + +main(_Args) -> + add_code_paths(), + {ok, _} = application:ensure_all_started(crypto), + {ok, _} = application:ensure_all_started(asn1), + {ok, _} = application:ensure_all_started(public_key), + OutputDir = <<"_build/preloaded-store">>, + SrcDir = <<"src/preloaded">>, + Wallet = hb:wallet(), + ?event(preload, {scanning_preloaded_devices, {dir, SrcDir}}), + Groups = hb_packager:scan([SrcDir], #{}), + ?event(preload, {packaging_preloaded_devices, {count, length(Groups)}}), + {ok, Result} = + hb_preload:build_groups( + Groups, + Wallet, + OutputDir, + #{ <<"bootstrap-device-src">> => [SrcDir] } + ), + lists:foreach( + fun(Pkg) -> + ?event(preload, { + packaged_preloaded_device, + {name, maps:get(device_name, Pkg)}, + {module, maps:get(module_name, Pkg)} + }) + end, + maps:get(pkgs, Result) + ), + Index = maps:get(index, Result), + ?event(preload, {preloaded_index, Index}), + HeaderPath = <<"_build/hb_preloaded_index.hrl">>, + ok = hb_preload:write_index_header(Index, HeaderPath), + ?event(preload, {preloaded_index_header, HeaderPath}), + recompile_hb_opts(), + halt(0). + +add_code_paths() -> + DefaultPaths = + filelib:wildcard("_build/default/lib/*/ebin") ++ + filelib:wildcard("_build/default/plugins/*/ebin"), + AllPaths = + filelib:wildcard("_build/*/lib/*/ebin") ++ + filelib:wildcard("_build/*/plugins/*/ebin"), + Paths = DefaultPaths ++ lists:sort(fun newer_path/2, AllPaths -- DefaultPaths), + lists:foreach( + fun(P) -> code:add_pathz(P) end, + Paths + ). + +newer_path(A, B) -> + newest_beam_time(A) >= newest_beam_time(B). + +newest_beam_time(Path) -> + case filelib:wildcard(filename:join(Path, "*.beam")) of + [] -> filelib:last_modified(Path); + Beams -> lists:max([filelib:last_modified(B) || B <- Beams]) + end. + +recompile_hb_opts() -> + lists:foreach( + fun(Ebin) -> + {ok, hb_opts} = + compile:file( + "src/core/resolver/hb_opts.erl", + [{outdir, Ebin} | hb_opts_compile_opts(Ebin)] + ) + end, + filelib:wildcard("_build/*/lib/hb/ebin") + ). + +hb_opts_compile_opts(Ebin) -> + Beam = filename:join(Ebin, "hb_opts.beam"), + case beam_lib:chunks(Beam, [compile_info]) of + {ok, {_, [{compile_info, Info}]}} -> + drop_outdir(proplists:get_value(options, Info, fallback_opts())); + _ -> + fallback_opts() + end. + +drop_outdir([{outdir, _} | Rest]) -> drop_outdir(Rest); +drop_outdir([Opt | Rest]) -> [Opt | drop_outdir(Rest)]; +drop_outdir([]) -> []. + +fallback_opts() -> + [ + debug_info, + {i, "src/core"} + ]. diff --git a/scripts/dynamic-router.lua b/scripts/dynamic-router.lua index 4e0ca921b..a3766094b 100644 --- a/scripts/dynamic-router.lua +++ b/scripts/dynamic-router.lua @@ -1,7 +1,7 @@ --- A dynamic route generator in an AO `~process@1.0'. --- This generator grants a routing table, found at `/now/routes', that is --- compatible with the `~router@1.0' interface. Subsequently, it can be ---- used for routing by HyperBEAM nodes' via setting the `route-provider' +--- used for routing by HyperBEAM nodes' via setting the `router@1.0/provider' --- node message key. --- --- The configuration options are as follows: @@ -27,7 +27,7 @@ local function ensure_defaults(state) state.routes = state.routes or {} state["is-admissible"] = state["is-admissible"] or { - path = "/default", + path = "default", default = "true" } state["sampling-rate"] = state["sampling-rate"] or 0.1 @@ -203,11 +203,25 @@ end -- Register a new host to a route. function register(state, assignment, opts) state = ensure_defaults(state) + ao.event({"register", { state = state, assignment = assignment, opts = opts }}) local req = assignment.body + + -- If the message is signed by an explicitly trusted peer, we can skip the + -- is-admissible check. + if state["trusted-peer"] then + local committers = ao.get("committers", req) + for _, committer in ipairs(committers) do + if committer == state["trusted-peer"] then + state = add_node(state, req) + return recalculate(state, assignment, opts) + end + end + end + req.path = state["is-admissible"].path or "is-admissible" local status, is_admissible = ao.resolve(state["is-admissible"], req) - ao.event("is-admissible result:", { status, is_admissible }) + ao.event({"is-admissible result:", { status, is_admissible }}) if status == "ok" and is_admissible == "true" then state = add_node(state, req) return recalculate(state, assignment, opts) @@ -270,11 +284,11 @@ function duration(state, assignment, opts) end function compute(state, assignment, opts) - if assignment.body.path == "register" then + if assignment.body.action == "register" then return register(state, assignment, opts) - elseif assignment.body.path == "recalculate" then + elseif assignment.body.action == "recalculate" then return recalculate(state, assignment, opts) - elseif assignment.body.path == "performance" then + elseif assignment.body.action == "performance" then return duration(state, assignment, opts) else -- If we have been called without a relevant path, simply ensure that @@ -377,7 +391,7 @@ function performance_test() -- Record the starting scores for the nodes local t0_node1_score = state.routes[1].nodes[1].weight local t0_node2_score = state.routes[1].nodes[1].weight - + if t0_node1_score ~= t0_node2_score then error("Initial node scores should be equal. Received: " .. tostring(t0_node1_score) .. " and " .. tostring(t0_node2_score)) @@ -403,7 +417,7 @@ function performance_test() state = state }} ) - + -- now trigger a recalc _, state = recalculate(state, { body = { path = "recalculate" } }, {}) @@ -429,6 +443,6 @@ function performance_test() if t1_node2_score >= t0_node2_score then error("Node 2 score should have decreased!") end - + return "ok" end \ No newline at end of file diff --git a/scripts/hyper-token-p4-client.lua b/scripts/hyper-token-p4-client.lua new file mode 100644 index 000000000..d6a14ce9e --- /dev/null +++ b/scripts/hyper-token-p4-client.lua @@ -0,0 +1,41 @@ +--- A simple script that can be used as a `~p4@1.0` ledger device, marshalling +--- requests to a local process. + +-- Find the user's balance in the current ledger state. +function balance(base, request) + local status, res = ao.resolve({ + path = + base["ledger-path"] + .. "/now/balance/" + .. request["target"] + }) + ao.event({ "client received balance response", + { status = status, res = res, target = request["target"] } } + ) + -- If the balance request fails (most likely because the user has no balance), + -- return a balance of 0. + if status ~= "ok" then + return "ok", 0 + end + + -- We have successfully retrieved the balance, so return it. + return "ok", res +end + +-- Charge the user's balance in the current ledger state. +function charge(base, request) + ao.event("debug_charge", { + "client starting charge", + { request = request, base = base } + }) + local status, res = ao.resolve({ + path = "(" .. base["ledger-path"] .. ")/push", + method = "POST", + body = request + }) + ao.event("debug_charge", { + "client received charge response", + { status = status, res = res } + }) + return "ok", res +end \ No newline at end of file diff --git a/scripts/hyper-token-p4.lua b/scripts/hyper-token-p4.lua new file mode 100644 index 000000000..d64619952 --- /dev/null +++ b/scripts/hyper-token-p4.lua @@ -0,0 +1,65 @@ +--- An extension to the `hyper-token.lua` script, for execution with the +--- `lua@5.3a` device. This script adds the ability for an `admin' account to +--- charge a user's account. This is useful for allowing a node operator to +--- collect fees from users, if they are running in a trusted execution +--- environment. +--- +--- This script must be added as after the `hyper-token.lua` script in the +--- `process-definition`s `script` field. + +-- Process an `admin' charge request: +-- 1. Verify the sender's identity. +-- 2. Ensure that the quantity and account are present in the request. +-- 3. Debit the source account. +-- 4. Increment the balance of the recipient account. +function charge(base, assignment) + ao.event({ "debug_charge", { "Charging", { assignment = assignment } } }) + + -- Verify that the request is signed by the admin. + local admin = base.admin + local charge_req = assignment.body + local _, committers = ao.resolve(charge_req, "committers") + ao.event({ "debug_charge", { "Validating charge requester: ", { + admin = admin, + committers = committers, + ["charge-request"] = charge_req, + } }}) + + if count_common(committers, admin) ~= 1 then + return "error", base + end + + local status, res, request = validate_request(base, assignment) + if status ~= "ok" then + return status, res + end + + -- Ensure that the quantity and account are present in the request. + if not request.quantity or not request.account then + ao.event({ "Failure: Quantity or account not found in request.", + { request = request } }) + base.result = { + status = "error", + error = "Quantity or account not found in request." + } + return "ok", base + end + + -- Debit the source. Note: We do not check the source balance here, because + -- the node is capable of debiting the source at-will -- even it puts the + -- source into debt. This is important because the node may estimate the + -- cost of an execution at lower than its actual cost. Subsequently, the + -- ledger should at least debit the source, even if the source may not + -- deposit to restore this balance. + ao.event({ "Debit request validated: ", { assignment = assignment } }) + base.balance = base.balance or {} + base.balance[request.account] = + (base.balance[request.account] or 0) - request.quantity + + -- Increment the balance of the recipient account. + base.balance[request.recipient] = + (base.balance[request.recipient] or 0) + request.quantity + + ao.event("debug_charge", { "Charge processed: ", { balances = base.balance } }) + return "ok", base +end diff --git a/scripts/hyper-token.lua b/scripts/hyper-token.lua new file mode 100644 index 000000000..237542a0a --- /dev/null +++ b/scripts/hyper-token.lua @@ -0,0 +1,898 @@ +--- ## HyperTokens: Networks of fungible, parallel ledgers. +--- # Version: 0.1. +--- +--- An AO token standard implementation, with support for sub-ledger networks, +--- executed with the `~lua@5.3` device. This script supports both the base token +--- blueprint's 'active' keys, as well as the mainnet sub-ledger API. +--- +--- Data access actions (e.g. `balance', `info', `total-supply') are not +--- implemented due to their redundancy. Instead, the full state of the process +--- is available via the AO-Core HTTP API, including all metadata and individual +--- account balances. +--- +--- A full description of the hyper-token standard can be found in the +--- `token.md` file in this directory. The remainder of this module document +--- provides a breif overview of its design, and focuses on its implementation +--- details. +--- +--- ## Design and Implementation +--- +--- If running as a `root' token (indicated by the absence of a `token' field), +--- the `balance' field should be initialized to a table of balances for the +--- token during spawning. The `ledgers' field holds the state of a sub-ledger's +--- own balances with other ledgers. This field is always initialized to a message +--- of zero balances during the evaluation of the first assignment of the process. +--- When the token receives a `credit-notice' message, it will interpret it as a +--- deposit from the sending ledger and update its record of its own balance with +--- the sending ledger. +--- +--- Atop the standard token transfer messages, the sub-ledger API allows for +--- `transfer' messages to specify a `route' field, which is a list of ledger +--- IDs that the transfer should be routed through in order to reach a +--- recipient on a different ledger. At each `hop' in the route, the recipient +--- ledger validates whether it trusts the sending ledger and whether it knows +--- how to route to the next hop. If the recipient ledger does not trust the +--- sending ledger, it will terminate the route with a `route-termination' +--- message. If the recipient ledger knows how to route to the next hop, it +--- will create a new `transfer' message to the next hop, with the first +--- ledger from the route removed and the remainder of the route and recipient +--- and quantity to transfer forwarded along with the message. +--- +--- There are three security checks performed on incoming messages, above the +--- standard balance transfer checks: +--- +--- 1. Assignments are evaluated against the `assess/assignment' message, if +--- present. If not, the assignment is evaluated against the process's own +--- scheduler address. +--- +--- 2. If the message does not originate from an end-user (indicated by the +--- presence of a `from-process' field), the message is evaluated against +--- the `assess/request' message, if present. If not, the message is +--- evaluated against the `authority' field. The `authority' field may +--- contain a list of addresses or messages that are considered to be +--- authorities. +--- +--- 3. `credit-notice' messages that do not originate from a sub-ledger's +--- `token' are evaluated for parity of source code with the receiving +--- ledger. This is achieved by comparing the `from-base' field of the +--- credit-notice message with `process/id&committers=none' on the +--- receiving ledger. + +--- Utility functions: + +-- Add a message to the outbox of the given base. +local function send(base, message) + table.insert(base.results.outbox, message) + return base +end + +-- Add a log message to the results of the given base. +local function log_result(base, status, message) + ao.event("token_log", {"Token action log: ", { + status = status, + message = message + }}) + base.results = base.results or {} + base.results.status = status + + if base.results.log then + table.insert(base.results.log, message) + else + base.results.log = { message } + end + + return base +end + +-- Normalize a quantity value to ensure it is a proper integer. +-- Returns either the normalized integer value or nil and an error message. +local function normalize_int(value) + local num + -- Handle string conversion + if type(value) == "string" then + -- Check for decimal part (not allowed) + if string.find(value, "%.") then + return nil + end + -- Convert to number + num = tonumber(value) + if not num then + return nil + end + elseif type(value) == "number" then + num = value + -- Check if it's an integer + if num ~= math.floor(num) then + return nil + end + else + -- Any other type is invalid. + return nil + end + + return num +end + +-- Count the number of elements in `a' that are also in `b'. +function count_common(a, b) + -- Normalize both arguments to tables. + if type(a) ~= "table" then a = { a } end + if type(b) ~= "table" then b = { b } end + + local count = 0 + for _, v in ipairs(a) do + for _, w in ipairs(b) do + if v == w then + count = count + 1 + end + end + end + + return count +end + +-- Normalize an argument to a table if it is not already a table. +local function normalize_table(value) + -- If value is already a table, return it. If it is not a string, return + -- a table containing only the value. + if type(value) == "table" then + ao.event({ "Table already normalized", { table = value } }) + return value + elseif type(value) ~= "string" then + return { value } + end + + -- If value is a string, remove quotes and split by comma. + local t = {} + local pos = 1 + local len = #value + while pos <= len do + -- find next comma + local comma_start, comma_end = value:find(",", pos, true) + local chunk + if comma_start then + chunk = value:sub(pos, comma_start - 1) + pos = comma_end + 1 + else + chunk = value:sub(pos) + pos = len + 1 + end + + -- trim whitespace and quotes + chunk = chunk:gsub("[\"']", "") + local trimmed = chunk:match("^%s*(.-)%s*$") + -- convert to number if possible + local num = tonumber(trimmed) + table.insert(t, num or trimmed) + end + + ao.event({ "Normalized table", { table = t } }) + return t +end + +--- Security verification functions: + +-- Enforce that a given list satisfies `hyper-token's grammar constraints. +-- This function is used to check that the `authority' and `scheduler' fields +-- satisfy the constraints specified by the `authority[-*]' and `scheduler[-*]' +-- fields. The supported grammar constraints are: +-- - `X`: A list of `X`s that are admissible to match in the subject. +-- - `X-match`: A count of the number of `X`s that must be present in the subject. +-- Default: Length of `X`. +-- - `X-required`: A list of `X`s that must be present in the subject. +-- Default: `{}`. +local function satisfies_list_constraints(subject, all, required, match) + -- Normalize the fields to tables, aside from the match count. + subject = normalize_table(subject) + all = normalize_table(all) + required = normalize_table(required or {}) + -- Normalize the match count. + match = match or #all + match = normalize_int(match) + + ao.event({ "Satisfies list constraints", { + subject = subject, + all = all, + match = match + }}) + + -- Check that the subject satisfies the grammar's constraints. + -- 1. The subject must have at least `match' elements in common with `all'. + -- 2. The subject must contain all elements in `required'. + local count = count_common(subject, all) + local required_count = count_common(required, subject) + + ao.event({ "Counts", { + subject = subject, + all = all, + required = required, + match = match, + count = count, + required_count = required_count + }}) + + return (count >= match) and (required_count == #required) +end + +-- Ensure that a message satisfies the grammar's constraints, or the assessment +-- message returns true, if present. +local function satisfies_constraints(message, assess, all, required, match) + -- If the assessment message is present, run it against the message. + if assess then + ao.event({ "Running assessment message against request." }, + { assessment = assess, message = message }) + local status, result = ao.resolve(assess, message) + if (status == "ok") and (result == true) then + ao.event({ "Assessment of request passed." }, { + message = message, + status = status, + result = result + }) + return true + else + ao.event({ "Assessment of request failed.", { + message = message, + status = status, + result = result + }}) + return false + end + end + + -- If the assessment message is not present, check the signatures against + -- the requirements list and specifiers. + local satisfies_auth = satisfies_list_constraints( + ao.get("committers", message), + all, + required, + match + ) + + ao.event({ "Constraint satisfaction results", { + result = satisfies_auth, + message = message, + all_admissible = all, + required = required, + required_count = match + }}) + + return satisfies_auth +end + +-- Ensure that the `authority' field satisfies the `authority[-*]' constraints +-- (as supported by `satisfies_constraints') or that the assessment message +-- returns true. +local function is_trusted_compute(base, assignment) + return satisfies_constraints( + assignment.body, + (base.assess or {})["authority"], + base.authority, + base["authority-required"], + base["authority-match"] + ) +end + +-- Ensure that the assignment is trusted. Either by running the assessment +-- process, or by checking the signature against the process's own scheduler +-- address and those it explicitly trusts. +local function is_trusted_assignment(base, assignment) + return satisfies_constraints( + assignment, + (base.assess or {})["scheduler"], + base.scheduler, + base["scheduler-required"], + base["scheduler-match"] + ) +end + +-- Determine if the ledger indicated by `base` is the root ledger. +local function is_root(base) + return base.token == nil +end + +-- Ensure that a credit-notice from another ledger is admissible. It must either +-- be from our own root ledger, or from a sub-ledger that is precisely the same +-- as our own. +local function validate_new_peer_ledger(base, request) + ao.event({ "Validating peer ledger: ", { request = request } }) + + -- Check if the request is from the root ledger. + if is_root(base) or (base.token == request.from) then + ao.event({ "Peer is parent token. Accepting." }, { + request = request + }) + return true + end + + -- Calculate the expected base ID from the process's own `process` message, + -- modified to remove the `authority' and `scheduler' fields. + -- This ensures that the process we are receiving the `credit-notice` from + -- has the same structure as our own process. + ao.event({ "Calculating expected `base` from self", { base = base } }) + local status, proc, expected + status, proc = ao.resolve({"as", "message@1.0", base}, "process") + -- Reset the `authority' and `scheduler' fields to nil, to ensure that the + -- `base` message matches the structure created by `~push@1.0`. + proc.authority = nil + proc.scheduler = nil + status, expected = + ao.resolve( + proc, + { path = "id", committers = "none" } + ) + ao.event({ "Expected `from-base`", { status = status, expected = expected } }) + -- Check if the `from-base' field is present in the assignment. + if not request["from-base"] then + ao.event({ "`from-base` field not found in message", { + request = request + }}) + return false + end + + -- Check if the `from-base' field matches the expected ID. + local base_matches = request["from-base"] == expected + + if not base_matches then + ao.event("debug_base", { "Peer registration messages do not match", { + expected_base = expected, + received_base = request["from-base"], + process = proc, + request = request + }}) + return false + end + + -- Check that the `from-authority' and `from-scheduler' fields match the + -- expected values, to the degree specified by the `authority-match' and + -- `scheduler-match' fields. Additionally, the `authority-required' and + -- `scheduler-required' fields may be present in the base, the members of + -- which must be present in the `from-authority' and `from-scheduler' fields + -- respectively. + local authority_matches = satisfies_list_constraints( + request["from-authority"], + base.authority, + base["authority-required"], + base["authority-match"] + ) + local scheduler_matches = satisfies_list_constraints( + request["from-scheduler"], + base.scheduler, + base["scheduler-required"], + base["scheduler-match"] + ) + if (not authority_matches) or (not scheduler_matches) then + ao.event("debug_base", { "Peer security parameters do not match", { + expected_authority = base.authority, + received_authority = request["from-authority"], + expected_scheduler = base.scheduler, + received_scheduler = request["from-scheduler"], + scheduler_matches = scheduler_matches, + authority_matches = authority_matches, + request = request + }}) + return false + end + + ao.event("Peer registration messages matches. Accepting.") + + return true +end + +-- Register a new peer ledger, if the `from-base' field matches our own. +local function register_peer(base, request) + -- Validate the registering ledger + if not validate_new_peer_ledger(base, request) then + base.results = { + status = "error", + error = "Ledger registration failed." + } + return "error", base + end + + -- Add to known ledgers + base.ledgers[request.from] = base.ledgers[request.from] or 0 + + return "ok", base +end + +-- Determine if a request is from a known ledger. Makes no assessment of whether +-- a request is otherwise trustworthy. +local function is_from_trusted_ledger(base, request) + -- We always trust the root ledger. + if request.from == base["token"] then + return true, base + end + + -- We trust any ledger that is already registered in the `ledgers' map. + if base.ledgers and (base.ledgers[request.from] ~= nil) then + return true, base + end + + -- Validate whether the request is from a new peer ledger. + local status + status, base = register_peer(base, request) + if status ~= "ok" then + return false, base + end + + return true, base +end + +-- Ensure that the ledger is initialized. +local function ensure_initialized(base, assignment) + -- Ensure that the base has a `result' field before we try to register. + base.results = base.results or {} + base.results.outbox = {} + base.results.status = "OK" + -- If the ledger is not being initialized, we can skip the rest of the + -- function. + if assignment.slot ~= 0 then + return "ok", base + end + base.balance = base.balance or {} + + -- Ensure that the `ledgers' map is initialized: present and empty. + base.ledgers = base.ledgers or {} + ao.event({ "Ledgers before initialization: ", base.ledgers }) + + for _, ledger in ipairs(base.ledgers) do + base.ledgers[ledger] = 0 + end + ao.event({ "Ledgers after initialization: ", base.ledgers }) + + if not base.token then + ao.event({ "Ledger has no source token. Skipping registration." }) + return "ok", base + end + + ao.event({ "Registering self with known token ledgers: ", { + ledgers = base.ledgers + }}) + + for _, ledger in ipairs(base.ledgers) do + -- Insert the register result into the base. + base.results = send(base, { + action = "Register", + target = ledger + }) + end + + return "ok", base +end + +-- Verify that an assignment has not been processed and that the request is +-- valid. If it is, update the `from' field to the address that signed the +-- request. +function validate_request(incoming_base, assignment) + -- Ensure that the ledger is initialized. + local status, base = ensure_initialized(incoming_base, assignment) + if status ~= "ok" then + return "error", log_result(incoming_base, "error", { + message = "Ledger initialization failed.", + assignment = assignment, + status = status, + }) + end + + -- First, ensure that the message has not already been processed. + ao.event("Deduplicating message.", { + ["history-length"] = #(base.dedup or {}) + }) + + status, base = + ao.resolve( + incoming_base, + {"as", + "dedup@1.0", + { + path = "compute", + ["subject-key"] = "body", + body = assignment.body + } + } + ) + + -- Set the device back to `process@1.0`. + base.device = "process@1.0" + if status ~= "ok" then + return "error", log_result(base, "error", { + message = "Deduplication failure.", + assignment = assignment, + status = status, + incoming_base = incoming_base, + resulting_base = base + }) + end + + -- Next, ensure that the assignment is trusted. + local trusted, details = is_trusted_assignment(base, assignment) + if not trusted then + return "error", log_result(base, "error", { + message = "Assignment is not trusted.", + details = details + }) + end + + if assignment.body["from-process"] then + -- If the request is proxied, we need to check that the source + -- computation is trusted. + trusted, details = is_trusted_compute(base, assignment) + if not trusted then + return "error", log_result(base, "error", { + message = "Message computation is not trusted.", + details = details + }) + end + assignment.body.from = assignment.body["from-process"] + return "ok", base, assignment.body + else + -- If the request is not proxied, we set the `from' field to the address + -- that signed the request. + local committers = ao.get("committers", assignment.body) + if #committers == 0 then + return "error", log_result(base, "error", { + message = "No request signers found." + }) + end + + -- Only accept single-signed requests to avoid ambiguity + if #committers > 1 then + return "error", log_result(base, "error", { + message = "Multiple signers detected, making sender ambiguous. " .. + "Only singly-signed requests are supported for end-user " .. + "requests (those that do not originate from another " .. + "computation)." + }) + end + + assignment.body.from = committers[1] + return "ok", base, assignment.body + end +end + +-- Ensure that the source has the required funds, then debit the source. Takes +-- an origin, which can be used to identify the reason for the debit in logging. +-- Returns error if the source balance is not viable, or `ok` and the updated +-- base state if the debit is successful. Does not credit any funds. +local function debit_balance(base, request) + local source = request.from + + ao.event({ "Attempting to deduct balance.", { + request = request, + balances = base.balance or {} + }}) + + -- Ensure that the `source' and `quantity' fields are present in the request. + if not source or not request.quantity then + return "error", log_result(base, "error", { + message = "Fund source or quantity not found in request.", + }) + end + + -- Normalize the quantity value. + request.quantity = normalize_int(request.quantity) + if not request.quantity then + ao.event({ "Invalid quantity value: ", { quantity = request.quantity } }) + base.results = { + status = "error", + error = "Invalid quantity value.", + quantity = request.quantity + } + return "error", base + end + + -- Ensure that the source has the required funds. + -- Check 1: The source balance is present in the ledger. + local source_balance = base.balance[source] + + if not source_balance then + return "error", log_result(base, "error", { + message = "Source balance not found.", + from = source, + quantity = request.quantity, + ["is-root"] = is_root(base) + }) + end + + -- Check 2: The source balance is a valid number. + if type(source_balance) ~= "number" then + return "error", log_result(base, "error", { + message = "Source balance is not a number.", + balance = source_balance + }) + end + + -- Check 3: Ensure that the quantity to deduct is a non-negative number. + if request.quantity < 0 then + return "error", log_result(base, "error", { + message = "Quantity to deduct is negative.", + quantity = request.quantity + }) + end + + -- Check 4: Ensure that the source has enough funds. + if source_balance < request.quantity then + return "error", log_result(base, "error", { + message = "Insufficient funds.", + from = source, + quantity = request.quantity, + balance = source_balance + }) + end + + ao.event({ "Deducting funds:", { request = request } }) + base.balance[source] = source_balance - request.quantity + ao.event({ "Balances after deduction:", + { balances = base.balance, ledgers = base.ledgers } } + ) + return "ok", base +end + +-- Transfer the specified amount from the given account to the given account, +-- optionally routing to a different sub-ledger if necessary. +-- There are four differing types of transfer requests. They have the following +-- semantics: +-- Balance == owed to X. Credit == Owed to subject by X. + +-- User on root -> User on sub-ledger: +-- Xfer in: Root = Dec User balance, Inc Sub-ledger balance +-- C-N in: Sub-ledger = Inc User balance + +-- User on sub-ledgerA -> User on sub-ledgerB: +-- Xfer in: Sub-ledgerA = Dec User balance +-- C-N in: Sub-ledgerB = Inc User balance + +-- User on sub-ledgerB -> User on sub-ledgerA: +-- Xfer in: Sub-ledgerB = Dec user balance +-- C-N in: Sub-ledgerA = Inc user balance + +-- User on A->B->C: +-- Xfer in: A = Dec User balance +-- C-N in: B = +-- C-N in: C = Inc User balance + +-- User on sub-ledger -> User on root: +-- Xfer in: Sub-ledger = Dec User balance +-- C-N in: Root = Inc User balance, Dec Sub-ledger balance +function transfer(base, assignment) + ao.event({ "Transfer request received", { assignment = assignment } }) + -- Verify the security of the request. + local status, request + status, base, request = validate_request(base, assignment) + if status ~= "ok" or not request then + return "ok", base + end + + -- Ensure that the recipient is known. + if not request.recipient then + return log_result(base, "error", { + message = "Transfer request has no recipient." + }) + end + + -- Normalize the quantity value. + local quantity = normalize_int(request.quantity) + if not quantity then + return log_result(base, "error", { + message = "Invalid quantity value.", + quantity = request.quantity + }) + end + + -- Ensure that the source has the required funds. If they do, debit them. + local debit_status + debit_status, base = debit_balance(base, request) + if debit_status ~= "ok" or base == nil then + return "ok", base + end + + if is_root(base) or not request.route then + -- We are the root ledger, or the user is sending tokens directly to + -- another user. We credit the recipient's balance, or the sub-ledger's + -- balance if the request has a `route' key. + local direct_recipient = request.route or request.recipient + base.balance[direct_recipient] = + (base.balance[direct_recipient] or 0) + quantity + base = send(base, { + action = "Credit-Notice", + target = direct_recipient, + recipient = request.recipient, + quantity = quantity, + sender = request.from + }) + return log_result(base, "ok", { + message = "Direct or root transfer processed successfully.", + from_user = request.from, + to = direct_recipient, + explicit_recipient = request.recipient, + quantity = quantity + }) + end + + if request.route == base.token then + -- The user is returning tokens to the root ledger, so we send a + -- transfer to the root ledger. + base = send(base, { + action = "Transfer", + target = base.token, + recipient = request.recipient, + quantity = string.format('%d', math.floor(request.quantity)) + }) + return log_result(base, "ok", { + message = "Ledger-root transfer processed successfully.", + from_user = request.from, + to_ledger = base.token, + to_user = request.recipient, + quantity = string.format('%d', math.floor(request.quantity)) + }) + end + + -- We are not the root ledger, and the request has a `route` key. + -- Subsequently, the target must be another ledger so we dispatch a + -- credit-notice to the peer ledger. The peer will increment the balance of + -- the recipient. + base = send(base, { + action = "Credit-Notice", + target = request.route, + recipient = request.recipient, + quantity = quantity, + sender = request.from + }) + + return log_result(base, "ok", { + message = "Ledger-ledger transfer processed successfully.", + from_user = request.from, + to_ledger = request.route, + to_user = request.recipient, + quantity = quantity + }) +end + +-- Process credit notices from other ledgers. +_G["credit-notice"] = function (base, assignment) + ao.event({ "Credit-Notice received", { assignment = assignment } }) + + -- Verify the security of the request. + local status, request + status, base, request = validate_request(base, assignment) + if status ~= "ok" or not request then + return "ok", base + end + + if is_root(base) then + -- The root ledger will not process credit notices. + return log_result(base, "error", { + message = "Credit-Notice to root ledger ignored." + }) + end + + -- Ensure that the recipient is known. + if not request.recipient then + return log_result(base, "error", { + message = "Credit-Notice request has no recipient." + }) + end + + -- Normalize the quantity value. + local quantity = normalize_int(request.quantity) + if not quantity then + return log_result(base, "error", { + message = "Invalid quantity value.", + quantity = request.quantity + }) + end + + -- Ensure that the sender is a trusted ledger peer. + local trusted + trusted, base = is_from_trusted_ledger(base, request) + if not trusted then + return log_result(base, "error", { + message = "Credit-Notice not from a trusted peer ledger." + }) + end + + -- Credit the recipient's balance. + base.balance[request.recipient] = + (base.balance[request.recipient] or 0) + quantity + + return "ok", log_result(base, "ok", { + message = "Credit-Notice processed successfully.", + from_ledger = request.from, + to_ledger = request.sender, + to_user = request.recipient, + quantity = quantity, + balance = base.balance[request.recipient] + }) +end + +-- Process registration requests from other ledgers. +function register(raw_base, assignment) + ao.event({ "Register request received", { assignment = assignment } }) + + local status, base, request = validate_request(raw_base, assignment) + if (status ~= "ok") or (type(request) ~= "table") then + return "ok", base + end + + if base.ledgers[request.from] then + ao.event({ "Ledger already registered. Ignoring registration request." }) + base.results = { + message = "Ledger already registered." + } + return "ok", base + end + + -- Validate the registering ledger + status, base = register_peer(base, request) + if status ~= "ok" then + return status, base + end + + -- Send a reciprocal registration request to the remote ledger. + base = send(base, { + target = request.from, + action = "register" + }) + + return "ok", base +end + +-- Register ourselves with a remote ledger, at the request of a user or another +-- ledger. +_G["register-remote"] = function (raw_base, assignment) + -- Validate the request. + local status, base, request = validate_request(raw_base, assignment) + if (status ~= "ok") or (type(request) ~= "table") then + return "ok", base + end + + base = log_result(base, "ok", { + message = "Register-Remote request received.", + peer = request.peer + }) + + -- Send a registration request to the remote ledger. Our request is simply + -- a `Register' message, as the recipient will be assessing our unsigned + -- process ID in order to validate that we are an appropriate peer. This is + -- added by our `push-device`, so no further action is required on our part. + base = send(base, { + target = request.peer, + action = "register" + }) + + return "ok", base +end + +--- Index function, called by the `~process@1.0` device for scheduled messages. +--- We route any `action' to the appropriate function based on the request path. +function compute(base, assignment) + local action = string.lower(assignment.body.action or "") + ao.event( + { + "compute called", + { + balance = base.balance, + ledgers = base.ledgers, + action = action + } + } + ) + + if action == "credit-notice" then + return _G["credit-notice"](base, assignment) + elseif action == "transfer" then + return transfer(base, assignment) + elseif action == "register" then + return register(base, assignment) + elseif action == "register-remote" then + return _G["register-remote"](base, assignment) + else + -- Handle unknown `action' values. + _, base = ensure_initialized(base, assignment) + base.results = { + status = "ok" + } + ao.event({ "Process initialized.", { slot = assignment.slot } }) + return "ok", base + end +end \ No newline at end of file diff --git a/scripts/hyper-token.md b/scripts/hyper-token.md new file mode 100644 index 000000000..770bf4bb4 --- /dev/null +++ b/scripts/hyper-token.md @@ -0,0 +1,217 @@ +# HyperTokens: Networks of fungible, parallel ledgers. +## Status: Draft-1 + +This document describes the implementation details of the token ledger found in +`scripts/token.lua`. The script is built for operation with the HyperBEAM +`~lua@5.3a` and `~process@1.0` devices. + +In addition to implementing the core AO token ledger API, `hyper-ledger.lua` also +implements a `sub-ledger` standard, which allows for the creation of networks +of ledgers that are may each hold fragments of the total supply of a given token. +Each ledger may execute in paralell fully asynchronously, while ownership in their +tokens can be viewed as fungible. The fungibility of tokens across these ledgers +is created as a result of their transitively enforced security properties -- each +ledger must be a precise copy of every other ledger in the network -- as well as +the transferrability of balances from one ledger to another. Ledgers that have +`register`ed with one another are able to transfer tokens directly. A multi-hop +routing option is also available for situations in which it may be desirable to +utilize pre-existing peer relationships instead. + +This document provides a terse overview of the mechanics of this standard, and +the specifics of its implementation in `scripts/token.lua`. + +## 1. Entities and State + +### 1.1 Entities +- **User Account**: Identified by wallet address, may own tokens in `ledgers`. +- **Ledger Process**: An AO process implementing this token script. +- **Root Ledger**: The base token ledger process, from which supply scarcity is + derived. Root ledgers are differentiated from `sub-ledger`s by the absence of + a `token` field. +- **Sub-Ledger**: An independent ledger that that may own tokens in the root + ledger, holding them on behalf of users and other ledgers in the network. + All sub-ledgers must have a `token' field in their process definition, which + contains the signed process ID of the root ledger. + +### 1.2 State + +Each ledger maintain the following fields: + +- `balance`: A message containing a map of user addresses to token balances. +- `ledgers`: A message containing a map of peer-ledgers and the local ledger's + balance in each. +- `token`: (Optional) The signed process ID of the root ledger. + +Additionally, ledgers (root or sub-ledger) may maintain any other metadata fields +as needed in their process definition messages. Both metadata and necessary +fields are available via the AO-Core HTTP API. + +## 2. Message Paths and Actions + +All instances of this script support calling the following functions, as either +the `path` or `action` of scheduled messages upon them: + +- **Register**: Attempt to establish a trust relationship between ledger peers. +- **Register-Remote**: User-initiated request for the recipient ledger to + register with a specific remote peer. After registration, the user may transfer + tokens from their own account on the registrant ledger, to the remote ledger + that they specified. +- **Transfer**: Move tokens between accounts (with optional routing). +- **Credit-Notice**: Notification sent by a ledger to a recipient of credit upon + a successful transfer. +- **Debit-Notice**: A notification granted by a ledger to a sender of tokens, + upon successful transfer. +- **Credit-Notice-Rejection**: A notice sent by a ledger to the sender of tokens, + if the ledger is unable to accept the transfer (crediting its own account). + This action occurs when the recipient ledger is unable to validate the sender + as a valid peer, or when the recipient ledger is unable to validate the + quantity of tokens being transferred. Upon receipt, a process will likely + wish to reverse the transfer locally. +- **Route-Termination**: Notice, dispatched to a sender of tokens, if the + ledger is unable to forward the transfer to the next hop in the stated route. + Upon receipt, senders will typically wish to send a new transfer request to + the ledger with a different route to reach their recipient. If all other + routes are exhausted, the sender may transfer the tokens to the root ledger. + +## 3. Core Process Flows + +### 3.1 Ledger Registration and Trust Negotiation. + +1. Ledger A sends `Register` to Ledger B. The `push-device` that delivers this + message to the recipient (for example, `push@1.0`) must add the + `from-process-uncommitted` field to the message, containing the hash of the + sending process. +2. Ledger B validates: + - The admissibility of the assignment (its `scheduler` commitment), + - Whether it trusts the computation commitments upon the message, and + - Whether the message is from a process that is executing precisely the same + code as the recipient, as signified by the `from-process-uncommitted` field. +3. Ledger B adds Ledger A to its list of trusted peers, if it is not already + present, and sends a reciprocal `Register` message to the sending ledger. +4. Ledger A validates and adds Ledger B using the same mechanism, then adds it + to its list of trusted peers. +5. **Result**: Bidirectional trust relationship, and the ability to transfer + tokens directly between the two ledgers. + +### 3.2 Direct Cross-Process Transfer + +**Objective**: `Alice` wants to send tokens to `Bob`, who is on Ledger B. There +is an established peer relationship between Ledger A and Ledger B. + +1. `Alice`, with tokens resident on Ledger A, initiates transfer to `Bob`, who + would like to receive tokens on Ledger B. Ledger B already has an established + trust relationship and sufficient balance for `Alice` to send tokens to `Bob`. +2. Ledger A validates request, checks their balance for `Alice`, and debits the + sender's account. +3. Ledger A sends a `Transfer` message to Ledger B. +4. Ledger B validates sender ledger, decrements sender's balance, and credits the + recipient's balance. + +### 3.3 Multi-Hop Transfer + +**Objective**: `Alice` wants to send tokens to `Bob`, who is on Ledger N, but +there is no peer relationship between Ledger A and Ledger N. + +1. `Alice` initiates a transfer on Ledger A, with a multi-hop route + (`route=[L₁, L₂, ..., Lₙ]`) to reach `Bob` on Ledger N. +2. As with S`3.2`, Ledger A validates request, checks their balance for `Alice`, + and debits the sender's account. +3. Each intermediate ledger in the route validates the balance of the sending + ledger, and debits their account. Each ledger also removes themselves from the + list of hops remaining in the `route` parameter of the request, and forwards + it onwards to the next hop. +4. Final ledger in the route validates the balance of the sending ledger, and + credits the `Bob`'s account. + +### 3.4 Transfer with Peer Registration + +**Objective**: `Alice` wants to send tokens to `Bob`, who is on Ledger B. There +is no peer relationship between Ledger A and Ledger B, but `Alice` would rather +establish one than route the transfer through a potentially longer multi-hop +route. + +1. `Alice` sends a `Register-Remote` message to Ledger B, with a `peer` parameter + of Ledger A's signed process ID. +2. Ledger B validates the request, and adds Ledger A to its list of trusted peers. +3. `Alice` sends a `Transfer` message to Ledger B. +4. Ledger A and B validate the transfer request, as in `3.2`. + +## 4. Intended Security Properties + +1. **Code Integrity**: Each new peer relationship validates that each other + peer is executing precisely the same code as it is. Subsequently, the + security properties of the original ledger are transitively applied to all + new ledgers in the network. +2. **Conservation of Tokens**: As each peer may trust each other peer to monitor + balances as they would, the network as a whole maintains the conservation + of the total supply of tokens. +3. **Trustless Registration**: A subledger may register with any AO token process + without that process needing to be aware of the subledger protocol, nor the + security properties that it enforces. Instead, users that wish to participate + in a subledger process network with security properties that they deem + acceptable may do so at-will. Processes that do choose to participate may + maintain an index of known ledgers, allowing tokens within them to be shown + as fungible with the root ledger's tokens in user interfaces, etc. + +## 5. API Reference + +### 5.1 External API Functions + +#### `transfer(state, assignment)` + +Transfers tokens from one account to another. + +Parameters: + +- `base`: The current state of the ledger process. +- `assignment`: An assignment of the transfer message from the process's + `sheduler`. +- `assignment/body`: The transfer message. +- `assignment/body/from`: Source account (determined from signature or + `from-process`). +- `assignment/body/recipient`: The destination account. +- `assignment/body/quantity`: Amount to transfer (integer). +- `assignment/body/route`: (Optional) A list of ledger IDs that should be + traversed to reach the destination ledger. + +Returns: The updated process ledger state, and: + +- `result/status`: The status of the transfer. +- `result/outbox/1`: A message containing the `Action: Credit-Notice` field, sent + to the recipient (either an end-user or another ledger). +- `result/outbox/2`: (Optional) A message containing the `Action: Debit-Notice` + field, dispatched to the sender _if_ the transfer is not a multi-hop route. + In the case of multi-hop routes, debit notices are not sent to any intermediate + ledgers, but are sent to the initial sender upon completion of the final hop. + +In event of error, the following messages are dispatched: + +- `result/outbox/1`: (Optional) A message containing the + `Action: Credit-Notice-Rejection` field, along with a `notice` field with the + ID of the credit notice that was rejected. +- `result/outbox/1`: (Optional) A message containing `Action: Route-Termination`, + sent to the initiator of an inter-ledger transfer, if the transfer is unable to + reach the destination ledger. In this case, the sender will hold a balance on + the intermediate ledger, and may attempt to route the transfer again with a + different route. + +#### `register-remote(base, assignment)` + +Initiates a registration from the target ledger to the `peer` ledger. + +Parameters: +- `base`: The current state of the ledger +- `assignment`: The message containing: +- `assignment/body/peer`: The signed process ID of the remote ledger to + register with. + +#### `register(base, assignment)` + +Attempts to register the sending ledger with the target ledger. + +Parameters: +- `base`: The current state of the ledger +- `assignment`: The message containing: +- `assignment/body/from`: The ledger requesting registration. +- `assignment/body/from-process-uncommitted`: A hash, added by the `push-device`, + of the sending process. \ No newline at end of file diff --git a/scripts/p4-payment-client.lua b/scripts/p4-payment-client.lua deleted file mode 100644 index 20a1921ae..000000000 --- a/scripts/p4-payment-client.lua +++ /dev/null @@ -1,84 +0,0 @@ ---- A simple script that can be used as a `~p4@1.0` ledger device, marshalling ---- requests to a local process. - --- Find the user's balance in the current ledger state. -function balance(base, request) - local status, res = ao.resolve({ - path = - base["ledger-path"] - .. "/now/balance/" - .. request["target"] - }) - ao.event({ "client received balance response", - { status = status, res = res, target = request["target"] } } - ) - -- If the balance request fails (most likely because the user has no balance), - -- return a balance of 0. - if status ~= "ok" then - return "ok", 0 - end - - -- We have successfully retrieved the balance, so return it. - return "ok", res -end - --- Debit the user's balance in the current ledger state. -function debit(base, request) - ao.event({ "client starting debit", { request = request, base = base } }) - local status, res = ao.resolve({ - path = "(" .. base["ledger-path"] .. ")/schedule", - method = "POST", - body = request - }) - ao.event({ "client received schedule response", { status = status, res = res } }) - status, res = ao.resolve({ - path = base["ledger-path"] .. "/compute/balance/" .. request["account"], - slot = res.slot - }) - ao.event({ "confirmed balance", { status = status, res = res } }) - return "ok" -end - ---- Poll an external ledger for credit events. If new credit noticess have been ---- sent by the external ledger, push them to the local ledger. -function poll(base, req) - local status, local_last_credit = ao.resolve({ - path = base["ledger-path"] .. "/now/last-credit" - }) - if status ~= "ok" then - ao.event( - { "error getting local last credit", - { status = status, res = local_last_credit } } - ) - return "error", base - end - - local status, external_last_credit = ao.resolve({ - path = base["external-ledger"] .. "/now/last-credit" - }) - if status ~= "ok" then - ao.event({ "error getting external last credit", - { status = status, res = external_last_credit } }) - return "error", base - end - - ao.event({ "Retreived sync data. Last credit info:", - { - local_last_credit = local_last_credit, - external_last_credit = external_last_credit } - } - ) - while local_last_credit < external_last_credit do - status, res = ao.resolve({ - path = base["external-ledger"] .. "/push", - slot = local_last_credit + 1 - }) - if status ~= "ok" then - ao.event({ "error pushing slot", { status = status, res = res } }) - return "error", base - end - local_last_credit = local_last_credit + 1 - end - - return "ok", base -end \ No newline at end of file diff --git a/scripts/p4-payment-process.lua b/scripts/p4-payment-process.lua deleted file mode 100644 index 7bb60c2a2..000000000 --- a/scripts/p4-payment-process.lua +++ /dev/null @@ -1,97 +0,0 @@ ---- A ledger that allows account balances to be debited and credited by a ---- specified address. - --- Check if the request is a valid debit/credit request by checking if one of --- the committers is the operator. -local function is_valid_request(base, assignment) - -- First, validate that the assignment is signed by the scheduler. - local scheduler = base.scheduler - local status, res = ao.resolve(assignment, "committers") - ao.event({ - "assignment committers resp:", - { status = status, res = res, scheduler = scheduler } - }) - - if status ~= "ok" then - return false - end - - local valid = false - for _, committer in ipairs(res) do - if committer == scheduler then - valid = true - end - end - - if not valid then - return false - end - - -- Next, validate that the request is signed by the operator. - local operator = base.operator - status, res = ao.resolve(assignment.body, "committers") - ao.event({ - "request committers resp:", - { status = status, res = res, operator = operator } - }) - - if status ~= "ok" then - return false - end - - for _, committer in ipairs(res) do - if committer == operator then - return true - end - end - - return false -end - --- Debit the specified account by the given amount. -function debit(base, assignment) - ao.event({ "process debit starting", { assignment = assignment } }) - if not is_valid_request(base, assignment) then - base.result = { status = "error", error = "Operator signature required." } - ao.event({ "debit error", base.result }) - return "ok", base - end - ao.event({ "process debit valid", { assignment = assignment } }) - base.balance = base.balance or {} - base.balance[assignment.body.account] = - (base.balance[assignment.body.account] or 0) - assignment.body.quantity - - ao.event({ "process debit success", { balances = base.balance } }) - return "ok", base -end - --- Credit the specified account by the given amount. -_G["credit-notice"] = function (base, assignment) - ao.event({ "credit-notice", { assignment = assignment }, { balances = base.balance } }) - if not is_valid_request(base, assignment) then - base.result = { status = "error", error = "Operator signature required." } - return "ok", base - end - ao.event({ "is valid", { req = assignment.body } }) - base.balance = base.balance or {} - base.balance[assignment.body.recipient] = - (base.balance[assignment.body.recipient] or 0) + assignment.body.quantity - ao.event({ "credit", { ["new balances"] = base.balance } }) - return "ok", base -end - ---- Index function, called by the `~process@1.0` device for scheduled messages. ---- We route each to the appropriate function based on the request path. -function compute(base, assignment, opts) - ao.event({ "compute", { assignment = assignment }, { balances = base.balance } }) - if assignment.body.path == "debit" then - return debit(base, assignment.body) - elseif assignment.body.path == "credit-notice" then - return _G["credit-notice"](base, assignment.body) - elseif assignment.body.path == "balance" then - return balance(base, assignment.body) - elseif assignment.slot == 0 then - base.balance = base.balance or {} - return "ok", base - end -end diff --git a/scripts/schema.gql b/scripts/schema.gql new file mode 100644 index 000000000..130f87c41 --- /dev/null +++ b/scripts/schema.gql @@ -0,0 +1,301 @@ +### Supported GraphQL Queries. ### + +type Query { + + # Get a message by its id or a subset of its keys. + message(id: ID, keys: [KeyInput]): Message + + # Get a transaction by its id + transaction(id: ID!): Transaction + + # Get a paginated set of matching transactions using filters. + transactions( + # Find transactions from a list of ids. + ids: [ID!] + + # Find transactions from a list of owner wallet addresses, or wallet owner public keys. + owners: [String!] + + # Find transactions from a list of recipient wallet addresses. + recipients: [String!] + + # Find transactions using tags. + tags: [TagFilter!] + + # Find data items from the given data bundles. + # See: https://github.com/ArweaveTeam/arweave-standards/blob/master/ans/ANS-104.md + bundledIn: [ID!] + + # Find transactions within a given Search Indexing Service ingestion time range. + ingested_at: RangeFilter + + # Find transactions within a given block height range. + block: RangeFilter + + # Result page size (max: 100) + first: Int = 10 + + # A pagination cursor value, for fetching subsequent pages from a result set. + after: String + + # Optionally specify the result sort order. + sort: SortOrder = HEIGHT_DESC + ): TransactionConnection! + block(id: String): Block + blocks( + # Find blocks from a list of ids. + ids: [ID!] + + # Find blocks within a given block height range. + height: RangeFilter + + # Result page size (max: 100) + first: Int = 10 + + # A pagination cursor value, for fetching subsequent pages from a result set. + after: String + + # Optionally specify the result sort order. + #sort: SortOrder = HEIGHT_DESC + ): BlockConnection! +} + +### HyperBEAM Message Schema. ### + +input KeyInput { + name: String! + value: String! +} + +type Message { + id: ID! + keys: [Key] +} + +type Key { + name: String + value: String +} + +### Arweave GraphQL Schema. ### + +# Indicates exactly one field must be supplied and this field must not be `null`. +directive @oneOf on INPUT_OBJECT + +directive @cacheControl( + maxAge: Int + scope: CacheControlScope + inheritMaxAge: Boolean +) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION + +# Representation of a transaction owner. +type Owner { + # The owner's wallet address. + address: String! + + # The owner's public key as a base64url encoded string. + key: String! +} + +# Representation of a value transfer between wallets, in both winson and ar. +type Amount { + # Amount as a winston string e.g. \`"1000000000000"\`. + winston: String! + + # Amount as an AR string e.g. \`"0.000000000001"\`. + ar: String! +} + +# Basic metadata about the transaction data payload. +type MetaData { + # Size of the associated data in bytes. + size: String! + + # Type is derived from the \`content-type\` tag on a transaction. + type: String +} + +# Tag Schema +type Tag { + # UTF-8 tag name + name: String! + + # UTF-8 tag value + value: String! +} + +# Block Schema +type Block { + # The block ID. + id: ID + + # The block timestamp (UTC). + timestamp: Int + + # The block height. + height: Int! + + # The previous block ID. + previous: ID +} + +# The parent transaction for bundled transactions, +# see: https://github.com/ArweaveTeam/arweave-standards/blob/master/ans/ANS-102.md. +type Parent { + id: ID! +} + +# The data bundle containing the current data item. +# See: https://github.com/ArweaveTeam/arweave-standards/blob/master/ans/ANS-104.md. +type Bundle { + # ID of the containing data bundle. + id: ID! +} + +# Transaction Structure +type Transaction { + id: ID! + anchor: String! + signature: String! + recipient: String! + owner: Owner! + fee: Amount! + quantity: Amount! + data: MetaData! + tags: [Tag!]! + + # When this transaction was made available for querying + ingested_at: Int + + # Transactions with a null block are recent and unconfirmed, if they aren't mined into a block within 60 minutes they will be removed from results. + block: Block + + # @deprecated Don't use, kept for backwards compatability only! + parent: Parent @deprecated(reason: "Use `bundledIn`") + + # For bundled data items this references the containing bundle ID. + # See: https://github.com/ArweaveTeam/arweave-standards/blob/master/ans/ANS-104.md + bundledIn: Bundle +} + +# Paginated page info using the GraphQL cursor spec. +type PageInfo { + hasNextPage: Boolean! +} + +# Paginated result set using the GraphQL cursor spec. +type TransactionEdge { + # The cursor value for fetching the next page. + # + # Pass this to the `after` parameter in ` transactions(after: $cursor)`, the next page will start from the next item after this. + cursor: String! + + # A transaction object. + node: Transaction! +} + +# Paginated result set using the GraphQL cursor spec, +# see: https://relay.dev/graphql/connections.htm. +type TransactionConnection { + pageInfo: PageInfo! + + # The number of transactions that match this query. + count: String + edges: [TransactionEdge!]! +} + +# Paginated result set using the GraphQL cursor spec. +type BlockEdge { + # The cursor value for fetching the next page. + # + # Pass this to the after parameter in blocks(after: $cursor), the next page will start from the next item after this. + cursor: String! + + # A block object. + node: Block! +} + +# Paginated result set using the GraphQL cursor spec, +# see: https://relay.dev/graphql/connections.htm. +type BlockConnection { + pageInfo: PageInfo! + edges: [BlockEdge!]! +} + +# Find transactions with the following tag name and value +input TagFilter { + # The tag name + name: String + + # An array of values to match against. If multiple values are passed then transactions with _any_ matching tag value from the set will be returned. + # + # e.g. + # + # `{name: "app-name", values: ["app-1"]}` + # + # Returns all transactions where the `app-name` tag has a value of `app-1`. + # + # `{name: "app-name", values: ["app-1", "app-2", "app-3"]}` + # + # Returns all transactions where the `app-name` tag has a value of either `app-1` _or_ `app-2` _or_ `app-3`. + values: [String!] + + # The operator to apply to to the tag filter. Defaults to EQ (equal). + #op: TagOperator! = EQ + + # How tag names and values are matched. Defaults to EXACT. + #match: TagMatch! = EXACT +} + +# The operator to apply to a tag value. +enum TagOperator { + # Equal + EQ + + # Not equal + NEQ +} + +# The method used to determine if tags match. +enum TagMatch { + # An exact match + EXACT + + # A wildcard match + WILDCARD + + # Fuzzy match containing all search terms + FUZZY_AND + + # Fuzzy match containing at least one search term + FUZZY_OR +} + +# Filter with a min and max +input RangeFilter { + # Minimum integer to filter from + min: Int + + # Maximum integer to filter to + max: Int +} + +# Optionally reverse the result sort order from `HEIGHT_DESC` (default) to `HEIGHT_ASC`. +enum SortOrder { + # Results are sorted by the transaction block height in ascending order, with the oldest transactions appearing first, and the most recent and pending/unconfirmed appearing last. + HEIGHT_ASC + + # Results are sorted by the transaction block height in descending order, with the most recent and unconfirmed/pending transactions appearing first. + HEIGHT_DESC + + # Results are sorted by the transaction ingestion time in descending order, with the most recently ingested transactions appearing first. + INGESTED_AT_DESC + + # Results are sorted by the transaction ingestion time in ascending order, with the oldest ingested transactions appearing first. + INGESTED_AT_ASC +} + +enum CacheControlScope { + PUBLIC + PRIVATE +} \ No newline at end of file diff --git a/src/ar_bundles.erl b/src/ar_bundles.erl deleted file mode 100644 index dc2435d25..000000000 --- a/src/ar_bundles.erl +++ /dev/null @@ -1,1091 +0,0 @@ --module(ar_bundles). --export([signer/1, is_signed/1]). --export([id/1, id/2, reset_ids/1, type/1, map/1, hd/1, member/2, find/2]). --export([manifest/1, manifest_item/1, parse_manifest/1]). --export([new_item/4, sign_item/2, verify_item/1]). --export([encode_tags/1, decode_tags/1]). --export([serialize/1, serialize/2, deserialize/1, deserialize/2]). --export([data_item_signature_data/1]). --export([normalize/1]). --export([print/1, format/1, format/2]). --include("include/hb.hrl"). --include_lib("eunit/include/eunit.hrl"). - -%%% @doc Module for creating, signing, and verifying Arweave data items and bundles. - --define(BUNDLE_TAGS, [ - {<<"bundle-format">>, <<"binary">>}, - {<<"bundle-version">>, <<"2.0.0">>} -]). - --define(LIST_TAGS, [ - {<<"map-format">>, <<"list">>} -]). - -% How many bytes of a binary to print with `print/1'. --define(BIN_PRINT, 20). --define(INDENT_SPACES, 2). - -%%%=================================================================== -%%% Public interface. -%%%=================================================================== - -print(Item) -> - io:format(standard_error, "~s", [lists:flatten(format(Item))]). - -format(Item) -> format(Item, 0). -format(Item, Indent) when is_list(Item); is_map(Item) -> - format(normalize(Item), Indent); -format(Item, Indent) when is_record(Item, tx) -> - Valid = verify_item(Item), - format_line( - "TX ( ~s: ~s ) {", - [ - if - Item#tx.signature =/= ?DEFAULT_SIG -> - lists:flatten( - io_lib:format( - "~s (signed) ~s (unsigned)", - [hb_util:encode(id(Item, signed)), hb_util:encode(id(Item, unsigned))] - ) - ); - true -> hb_util:encode(id(Item, unsigned)) - end, - if - Valid == true -> "[SIGNED+VALID]"; - true -> "[UNSIGNED/INVALID]" - end - ], - Indent - ) ++ - case (not Valid) andalso Item#tx.signature =/= ?DEFAULT_SIG of - true -> - format_line("!!! CAUTION: ITEM IS SIGNED BUT INVALID !!!", Indent + 1); - false -> [] - end ++ - case is_signed(Item) of - true -> - format_line("Signer: ~s", [hb_util:encode(signer(Item))], Indent + 1); - false -> [] - end ++ - format_line("Target: ~s", [ - case Item#tx.target of - <<>> -> "[NONE]"; - Target -> hb_util:id(Target) - end - ], Indent + 1) ++ - format_line("Tags:", Indent + 1) ++ - lists:map( - fun({Key, Val}) -> format_line("~s -> ~s", [Key, Val], Indent + 2) end, - Item#tx.tags - ) ++ - format_line("Data:", Indent + 1) ++ format_data(Item, Indent + 2) ++ - format_line("}", Indent); -format(Item, Indent) -> - % Whatever we have, its not a tx... - format_line("INCORRECT ITEM: ~p", [Item], Indent). - -format_data(Item, Indent) when is_binary(Item#tx.data) -> - case lists:keyfind(<<"bundle-format">>, 1, Item#tx.tags) of - {_, _} -> - format_data(deserialize(serialize(Item)), Indent); - false -> - format_line( - "Binary: ~p... <~p bytes>", - [format_binary(Item#tx.data), byte_size(Item#tx.data)], - Indent - ) - end; -format_data(Item, Indent) when is_map(Item#tx.data) -> - format_line("Map:", Indent) ++ - lists:map( - fun({Name, MapItem}) -> - format_line("~s ->", [Name], Indent + 1) ++ - format(MapItem, Indent + 2) - end, - maps:to_list(Item#tx.data) - ); -format_data(Item, Indent) when is_list(Item#tx.data) -> - format_line("List:", Indent) ++ - lists:map( - fun(ListItem) -> - format(ListItem, Indent + 1) - end, - Item#tx.data - ). - -format_binary(Bin) -> - lists:flatten( - io_lib:format( - "~p", - [ - binary:part( - Bin, - 0, - case byte_size(Bin) of - X when X < ?BIN_PRINT -> X; - _ -> ?BIN_PRINT - end - ) - ] - ) - ). - -format_line(Str, Indent) -> format_line(Str, "", Indent). -format_line(RawStr, Fmt, Ind) -> - io_lib:format( - [$\s || _ <- lists:seq(1, Ind * ?INDENT_SPACES)] ++ - lists:flatten(RawStr) ++ "\n", - Fmt - ). - -%% @doc Return the address of the signer of an item, if it is signed. -signer(#tx { owner = ?DEFAULT_OWNER }) -> undefined; -signer(Item) -> crypto:hash(sha256, Item#tx.owner). - -%% @doc Check if an item is signed. -is_signed(Item) -> - Item#tx.signature =/= ?DEFAULT_SIG. - -%% @doc Return the ID of an item -- either signed or unsigned as specified. -%% If the item is unsigned and the user requests the signed ID, we return -%% the atom `not_signed'. In all other cases, we return the ID of the item. -id(Item) -> id(Item, unsigned). -id(Item, Type) when not is_record(Item, tx) -> - id(normalize(Item), Type); -id(Item = #tx { unsigned_id = ?DEFAULT_ID }, unsigned) -> - CorrectedItem = reset_ids(Item), - CorrectedItem#tx.unsigned_id; -id(#tx { unsigned_id = UnsignedID }, unsigned) -> - UnsignedID; -id(#tx { id = ?DEFAULT_ID }, signed) -> - not_signed; -id(#tx { id = ID }, signed) -> - ID. - -%% @doc Return the first item in a bundle-map/list. -hd(#tx { data = #{ <<"1">> := Msg } }) -> Msg; -hd(#tx { data = [First | _] }) -> First; -hd(TX = #tx { data = Binary }) when is_binary(Binary) -> - ?MODULE:hd((deserialize(serialize(TX), binary))#tx.data); -hd(#{ <<"1">> := Msg }) -> Msg; -hd(_) -> undefined. - -%% @doc Convert an item containing a map or list into an Erlang map. -map(#tx { data = Map }) when is_map(Map) -> Map; -map(#tx { data = Data }) when is_list(Data) -> - maps:from_list( - lists:zipwith( - fun({Index, Item}) -> {integer_to_binary(Index), map(Item)} end, - lists:seq(1, length(Data)), - Data - ) - ); -map(Item = #tx { data = Data }) when is_binary(Data) -> - (maybe_unbundle(Item))#tx.data. - -%% @doc Check if an item exists in a bundle-map/list. -member(Key, Item) -> - find(Key, Item) =/= not_found. - -%% @doc Find an item in a bundle-map/list and return it. -find(Key, Map) when is_map(Map) -> - case maps:get(Key, Map, not_found) of - not_found -> find(Key, maps:values(Map)); - Item -> Item - end; -find(_Key, []) -> not_found; -find(Key, [Item|Rest]) -> - case find(Key, Item) of - not_found -> find(Key, Rest); - CorrectItem -> CorrectItem - end; -find(Key, Item = #tx { id = Key }) -> Item; -find(Key, Item = #tx { data = Data }) -> - case id(Item, unsigned) of - Key -> Item; - _ -> - case is_binary(Data) of - false -> find(Key, Data); - true -> not_found - end - end; -find(_Key, _) -> - not_found. - -%% @doc Return the manifest item in a bundle-map/list. -manifest_item(#tx { manifest = Manifest }) when is_record(Manifest, tx) -> - Manifest; -manifest_item(_Item) -> undefined. - -%% @doc Create a new data item. Should only be used for testing. -new_item(Target, Anchor, Tags, Data) -> - reset_ids( - #tx{ - format = ans104, - target = Target, - last_tx = Anchor, - tags = Tags, - data = Data, - data_size = byte_size(Data) - } - ). - -%% @doc Sign a data item. -sign_item(_, undefined) -> throw(wallet_not_found); -sign_item(RawItem, {PrivKey, {KeyType, Owner}}) -> - Item = (normalize_data(RawItem))#tx{format = ans104, owner = Owner, signature_type = KeyType}, - % Generate the signature from the data item's data segment in 'signed'-ready mode. - Sig = ar_wallet:sign(PrivKey, data_item_signature_data(Item, signed)), - reset_ids(Item#tx{signature = Sig}). - -%% @doc Verify the validity of a data item. -verify_item(DataItem) -> - ValidID = verify_data_item_id(DataItem), - ValidSignature = verify_data_item_signature(DataItem), - ValidTags = verify_data_item_tags(DataItem), - ValidID andalso ValidSignature andalso ValidTags. - -type(Item) when is_record(Item, tx) -> - lists:keyfind(<<"bundle-map">>, 1, Item#tx.tags), - case lists:keyfind(<<"bundle-map">>, 1, Item#tx.tags) of - {<<"bundle-map">>, _} -> - case lists:keyfind(<<"map-format">>, 1, Item#tx.tags) of - {<<"map-format">>, <<"list">>} -> list; - _ -> map - end; - _ -> - binary - end; -type(Data) when erlang:is_map(Data) -> - map; -type(Data) when erlang:is_list(Data) -> - list; -type(_) -> - binary. - -%%%=================================================================== -%%% Private functions. -%%%=================================================================== - -%% @doc Generate the data segment to be signed for a data item. -data_item_signature_data(RawItem) -> - data_item_signature_data(RawItem, signed). -data_item_signature_data(RawItem, unsigned) -> - data_item_signature_data(RawItem#tx { owner = ?DEFAULT_OWNER }, signed); -data_item_signature_data(RawItem, signed) -> - true = enforce_valid_tx(RawItem), - NormItem = normalize_data(RawItem), - ar_deep_hash:hash([ - utf8_encoded("dataitem"), - utf8_encoded("1"), - %% Only SignatureType 1 is supported for now (RSA 4096) - utf8_encoded("1"), - <<(NormItem#tx.owner)/binary>>, - <<(NormItem#tx.target)/binary>>, - <<(NormItem#tx.last_tx)/binary>>, - encode_tags(NormItem#tx.tags), - <<(NormItem#tx.data)/binary>> - ]). - -%% @doc Verify the data item's ID matches the signature. -verify_data_item_id(DataItem) -> - ExpectedID = crypto:hash(sha256, DataItem#tx.signature), - DataItem#tx.id == ExpectedID. - -%% @doc Verify the data item's signature. -verify_data_item_signature(DataItem) -> - SignatureData = data_item_signature_data(DataItem), - %?event({unsigned_id, hb_util:encode(id(DataItem, unsigned)), hb_util:encode(SignatureData)}), - ar_wallet:verify( - {DataItem#tx.signature_type, DataItem#tx.owner}, SignatureData, DataItem#tx.signature - ). - -%% @doc Verify the validity of the data item's tags. -verify_data_item_tags(DataItem) -> - ValidCount = length(DataItem#tx.tags) =< 128, - ValidTags = lists:all( - fun({Name, Value}) -> - byte_size(Name) =< 1024 andalso byte_size(Value) =< 3072 - end, - DataItem#tx.tags - ), - ValidCount andalso ValidTags. - -normalize(Item) -> reset_ids(normalize_data(Item)). - -%% @doc Ensure that a data item (potentially containing a map or list) has a standard, serialized form. -normalize_data(not_found) -> throw(not_found); -normalize_data(Bundle) when is_list(Bundle); is_map(Bundle) -> - ?event({normalize_data, bundle, Bundle}), - normalize_data(#tx{ data = Bundle }); -normalize_data(Item = #tx { data = Data }) when is_list(Data) -> - ?event({normalize_data, list, Item}), - normalize_data( - Item#tx{ - tags = add_list_tags(Item#tx.tags), - data = - maps:from_list( - lists:zipwith( - fun(Index, MapItem) -> - { - integer_to_binary(Index), - update_ids(normalize_data(MapItem)) - } - end, - lists:seq(1, length(Data)), - Data - ) - ) - } - ); -normalize_data(Item = #tx{data = Bin}) when is_binary(Bin) -> - ?event({normalize_data, binary, Item}), - normalize_data_size(Item); -normalize_data(Item = #tx{data = Data}) -> - ?event({normalize_data, map, Item}), - normalize_data_size( - case serialize_bundle_data(Data, Item#tx.manifest) of - {Manifest, Bin} -> - Item#tx{ - data = Bin, - manifest = Manifest, - tags = add_manifest_tags( - add_bundle_tags(Item#tx.tags), - id(Manifest, unsigned) - ) - }; - DirectBin -> - Item#tx{ - data = DirectBin, - tags = add_bundle_tags(Item#tx.tags) - } - end - ). - -%% @doc Reset the data size of a data item. Assumes that the data is already normalized. -normalize_data_size(Item = #tx{data = Bin}) when is_binary(Bin) -> - Item#tx{data_size = byte_size(Bin)}; -normalize_data_size(Item) -> Item. - -%% @doc Convert a #tx record to its binary representation. -serialize(not_found) -> throw(not_found); -serialize(TX) -> serialize(TX, binary). -serialize(TX, binary) when is_binary(TX) -> TX; -serialize(RawTX, binary) -> - true = enforce_valid_tx(RawTX), - TX = normalize(RawTX), - EncodedTags = encode_tags(TX#tx.tags), - << - (encode_signature_type(TX#tx.signature_type))/binary, - (TX#tx.signature)/binary, - (TX#tx.owner)/binary, - (encode_optional_field(TX#tx.target))/binary, - (encode_optional_field(TX#tx.last_tx))/binary, - (encode_tags_size(TX#tx.tags, EncodedTags))/binary, - EncodedTags/binary, - (TX#tx.data)/binary - >>; -serialize(TX, json) -> - true = enforce_valid_tx(TX), - hb_json:encode(hb_message:convert(TX, <<"ans104@1.0">>, #{})). - -%% @doc Take an item and ensure that it is of valid form. Useful for ensuring -%% that a message is viable for serialization/deserialization before execution. -%% This function should throw simple, easy to follow errors to aid devs in -%% debugging issues. -enforce_valid_tx(List) when is_list(List) -> - lists:all(fun enforce_valid_tx/1, List); -enforce_valid_tx(Map) when is_map(Map) -> - lists:all(fun(Item) -> enforce_valid_tx(Item) end, maps:values(Map)); -enforce_valid_tx(TX) -> - ok_or_throw(TX, - check_type(TX, message), - {invalid_tx, TX} - ), - ok_or_throw(TX, - check_size(TX#tx.id, [0, 32]), - {invalid_field, id, TX#tx.id} - ), - ok_or_throw(TX, - check_size(TX#tx.unsigned_id, [0, 32]), - {invalid_field, unsigned_id, TX#tx.unsigned_id} - ), - ok_or_throw(TX, - check_size(TX#tx.last_tx, [0, 32]), - {invalid_field, last_tx, TX#tx.last_tx} - ), - ok_or_throw(TX, - check_size(TX#tx.owner, [0, byte_size(?DEFAULT_OWNER)]), - {invalid_field, owner, TX#tx.owner} - ), - ok_or_throw(TX, - check_size(TX#tx.target, [0, 32]), - {invalid_field, target, TX#tx.target} - ), - ok_or_throw(TX, - check_size(TX#tx.signature, [0, 65, byte_size(?DEFAULT_SIG)]), - {invalid_field, signature, TX#tx.signature} - ), - lists:foreach( - fun({Name, Value}) -> - ok_or_throw(TX, - check_type(Name, binary), - {invalid_field, tag_name, Name} - ), - ok_or_throw(TX, - check_size(Name, {range, 0, ?MAX_TAG_NAME_SIZE}), - {invalid_field, tag_name, Name} - ), - ok_or_throw(TX, - check_type(Value, binary), - {invalid_field, tag_value, Value} - ), - ok_or_throw(TX, - check_size(Value, {range, 0, ?MAX_TAG_VALUE_SIZE}), - {invalid_field, tag_value, Value} - ); - (InvalidTagForm) -> - throw({invalid_field, tag, InvalidTagForm}) - end, - TX#tx.tags - ), - ok_or_throw( - TX, - check_type(TX#tx.data, binary) - orelse check_type(TX#tx.data, map) - orelse check_type(TX#tx.data, list), - {invalid_field, data, TX#tx.data} - ), - true. - -%% @doc Force that a binary is either empty or the given number of bytes. -check_size(Bin, {range, Start, End}) -> - check_type(Bin, binary) - andalso byte_size(Bin) >= Start - andalso byte_size(Bin) =< End; -check_size(Bin, Sizes) -> - check_type(Bin, binary) - andalso lists:member(byte_size(Bin), Sizes). - -%% @doc Ensure that a value is of the given type. -check_type(Value, binary) when is_binary(Value) -> true; -check_type(Value, _) when is_binary(Value) -> false; -check_type(Value, list) when is_list(Value) -> true; -check_type(Value, _) when is_list(Value) -> false; -check_type(Value, map) when is_map(Value) -> true; -check_type(Value, _) when is_map(Value) -> false; -check_type(Value, message) -> - is_record(Value, tx) or is_map(Value) or is_list(Value); -check_type(_Value, _) -> false. - -%% @doc Throw an error if the given value is not ok. -ok_or_throw(_, true, _) -> true; -ok_or_throw(_TX, false, Error) -> - throw(Error). - -%% @doc Take an item and ensure that both the unsigned and signed IDs are -%% appropriately set. This function is structured to fall through all cases -%% of poorly formed items, recursively ensuring its correctness for each case -%% until the item has a coherent set of IDs. -%% The cases in turn are: -%% - The item has no unsigned_id. This is never valid. -%% - The item has the default signature and ID. This is valid. -%% - The item has the default signature but a non-default ID. Reset the ID. -%% - The item has a signature. We calculate the ID from the signature. -%% - Valid: The item is fully formed and has both an unsigned and signed ID. -update_ids(Item = #tx { unsigned_id = ?DEFAULT_ID }) -> - update_ids( - Item#tx { - unsigned_id = - crypto:hash( - sha256, - data_item_signature_data(Item, unsigned) - ) - } - ); -update_ids(Item = #tx { id = ?DEFAULT_ID, signature = ?DEFAULT_SIG }) -> - Item; -update_ids(Item = #tx { signature = ?DEFAULT_SIG }) -> - Item#tx { id = ?DEFAULT_ID }; -update_ids(Item = #tx { signature = Sig }) when Sig =/= ?DEFAULT_SIG -> - Item#tx { id = crypto:hash(sha256, Sig) }; -update_ids(TX) -> TX. - -%% @doc Re-calculate both of the IDs for an item. This is a wrapper -%% function around `update_id/1' that ensures both IDs are set from -%% scratch. -reset_ids(Item) -> - update_ids(Item#tx { unsigned_id = ?DEFAULT_ID, id = ?DEFAULT_ID }). - -add_bundle_tags(Tags) -> ?BUNDLE_TAGS ++ (Tags -- ?BUNDLE_TAGS). - -add_list_tags(Tags) -> - (?BUNDLE_TAGS ++ (Tags -- ?BUNDLE_TAGS)) ++ ?LIST_TAGS. - -add_manifest_tags(Tags, ManifestID) -> - lists:filter( - fun - ({<<"bundle-map">>, _}) -> false; - (_) -> true - end, - Tags - ) ++ [{<<"bundle-map">>, hb_util:encode(ManifestID)}]. - -finalize_bundle_data(Processed) -> - Length = <<(length(Processed)):256/integer>>, - Index = <<<<(byte_size(Data)):256/integer, ID/binary>> || {ID, Data} <- Processed>>, - Items = <<<> || {_, Data} <- Processed>>, - <>. - -to_serialized_pair(Item) -> - % TODO: This is a hack to get the ID of the item. We need to do this because we may not - % have the ID in 'item' if it is just a map/list. We need to make this more efficient. - Serialized = serialize(reset_ids(normalize(Item)), binary), - Deserialized = deserialize(Serialized, binary), - UnsignedID = id(Deserialized, unsigned), - {UnsignedID, Serialized}. - -serialize_bundle_data(Map, _Manifest) when is_map(Map) -> - % TODO: Make this compatible with the normal manifest spec. - % For now we just serialize the map to a JSON string of Key=>TXID - BinItems = maps:map(fun(_, Item) -> to_serialized_pair(Item) end, Map), - Index = maps:map(fun(_, {TXID, _}) -> hb_util:encode(TXID) end, BinItems), - NewManifest = new_manifest(Index), - %?event({generated_manifest, NewManifest == Manifest, hb_util:encode(id(NewManifest, unsigned)), Index}), - {NewManifest, finalize_bundle_data([to_serialized_pair(NewManifest) | maps:values(BinItems)])}; -serialize_bundle_data(List, _Manifest) when is_list(List) -> - finalize_bundle_data(lists:map(fun to_serialized_pair/1, List)); -serialize_bundle_data(Data, _Manifest) -> - throw({cannot_serialize_tx_data, must_be_map_or_list, Data}). - -new_manifest(Index) -> - TX = normalize(#tx{ - format = ans104, - tags = [ - {<<"data-protocol">>, <<"bundle-map">>}, - {<<"variant">>, <<"0.0.1">>} - ], - data = hb_json:encode(Index) - }), - TX. - -manifest(Map) when is_map(Map) -> Map; -manifest(#tx { manifest = undefined }) -> undefined; -manifest(#tx { manifest = ManifestTX }) -> - hb_json:decode(ManifestTX#tx.data). - -parse_manifest(Item) when is_record(Item, tx) -> - parse_manifest(Item#tx.data); -parse_manifest(Bin) -> - hb_json:decode(Bin). - -%% @doc Only RSA 4096 is currently supported. -%% Note: the signature type '1' corresponds to RSA 4096 -- but it is is written in -%% little-endian format which is why we encode to `<<1, 0>>'. -encode_signature_type({rsa, 65537}) -> - <<1, 0>>; -encode_signature_type(_) -> - unsupported_tx_format. - -%% @doc Encode an optional field (target, anchor) with a presence byte. -encode_optional_field(<<>>) -> - <<0>>; -encode_optional_field(Field) -> - <<1:8/integer, Field/binary>>. - -%% @doc Encode a UTF-8 string to binary. -utf8_encoded(String) -> - unicode:characters_to_binary(String, utf8). - -encode_tags_size([], <<>>) -> - <<0:64/little-integer, 0:64/little-integer>>; -encode_tags_size(Tags, EncodedTags) -> - <<(length(Tags)):64/little-integer, (byte_size(EncodedTags)):64/little-integer>>. - -%% @doc Encode tags into a binary format using Apache Avro. -encode_tags([]) -> - <<>>; -encode_tags(Tags) -> - EncodedBlocks = lists:flatmap( - fun({Name, Value}) -> - Res = [encode_avro_string(Name), encode_avro_string(Value)], - case lists:member(error, Res) of - true -> - throw({cannot_encode_empty_string, Name, Value}); - false -> - Res - end - end, - Tags - ), - TagCount = length(Tags), - ZigZagCount = encode_zigzag(TagCount), - <>. - -%% @doc Encode a string for Avro using ZigZag and VInt encoding. -encode_avro_string(<<>>) -> - % Zero length strings are treated as a special case, due to the Avro encoder. - << 0 >>; -encode_avro_string(String) -> - StringBytes = unicode:characters_to_binary(String, utf8), - Length = byte_size(StringBytes), - <<(encode_zigzag(Length))/binary, StringBytes/binary>>. - -%% @doc Encode an integer using ZigZag encoding. -encode_zigzag(Int) when Int >= 0 -> - encode_vint(Int bsl 1); -encode_zigzag(Int) -> - encode_vint(Int bsl 1, -1). - -%% @doc Encode a ZigZag integer to VInt binary format. -encode_vint(ZigZag) -> - encode_vint(ZigZag, []). - -encode_vint(0, Acc) -> - list_to_binary(lists:reverse(Acc)); -encode_vint(ZigZag, Acc) -> - VIntByte = ZigZag band 16#7F, - ZigZagShifted = ZigZag bsr 7, - case ZigZagShifted of - 0 -> encode_vint(0, [VIntByte | Acc]); - _ -> encode_vint(ZigZagShifted, [VIntByte bor 16#80 | Acc]) - end. - -%% @doc Convert binary data back to a #tx record. -deserialize(not_found) -> throw(not_found); -deserialize(Binary) -> deserialize(Binary, binary). -deserialize(Item, binary) when is_record(Item, tx) -> - maybe_unbundle(Item); -deserialize(Binary, binary) -> - %try - {SignatureType, Signature, Owner, Rest} = decode_signature(Binary), - {Target, Rest2} = decode_optional_field(Rest), - {Anchor, Rest3} = decode_optional_field(Rest2), - {Tags, Data} = decode_tags(Rest3), - maybe_unbundle( - reset_ids(#tx{ - format = ans104, - signature_type = SignatureType, - signature = Signature, - owner = Owner, - target = Target, - last_tx = Anchor, - tags = Tags, - data = Data, - data_size = byte_size(Data) - }) - ); -%catch -% _:_:_Stack -> -% {error, invalid_item} -%end; -deserialize(Bin, json) -> - try - Map = hb_json:decode(Bin), - hb_message:convert(Map, <<"ans104@1.0">>, #{}) - catch - _:_:_Stack -> - {error, invalid_item} - end. - -maybe_unbundle(Item) -> - Format = lists:keyfind(<<"bundle-format">>, 1, Item#tx.tags), - Version = lists:keyfind(<<"bundle-version">>, 1, Item#tx.tags), - case {Format, Version} of - {{<<"bundle-format">>, <<"binary">>}, {<<"bundle-version">>, <<"2.0.0">>}} -> - maybe_map_to_list(maybe_unbundle_map(Item)); - _ -> - Item - end. - -maybe_map_to_list(Item) -> - case lists:keyfind(<<"map-format">>, 1, Item#tx.tags) of - {<<"map-format">>, <<"List">>} -> - unbundle_list(Item); - _ -> - Item - end. - -unbundle_list(Item) -> - Item#tx{ - data = - lists:map( - fun(Index) -> - maps:get(list_to_binary(integer_to_list(Index)), Item#tx.data) - end, - lists:seq(1, maps:size(Item#tx.data)) - ) - }. - -maybe_unbundle_map(Bundle) -> - case lists:keyfind(<<"bundle-map">>, 1, Bundle#tx.tags) of - {<<"bundle-map">>, MapTXID} -> - case unbundle(Bundle) of - detached -> Bundle#tx { data = detached }; - Items -> - MapItem = find_single_layer(hb_util:decode(MapTXID), Items), - Map = hb_json:decode(MapItem#tx.data), - Bundle#tx{ - manifest = MapItem, - data = - maps:map( - fun(_K, TXID) -> - find_single_layer(hb_util:decode(TXID), Items) - end, - Map - ) - } - end; - _ -> - unbundle(Bundle) - end. - -%% @doc An internal helper for finding an item in a single-layer of a bundle. -%% Does not recurse! You probably want `find/2' in most cases. -find_single_layer(UnsignedID, TX) when is_record(TX, tx) -> - find_single_layer(UnsignedID, TX#tx.data); -find_single_layer(UnsignedID, Items) -> - TX = lists:keyfind(UnsignedID, #tx.unsigned_id, Items), - case is_record(TX, tx) of - true -> TX; - false -> - throw({cannot_find_item, hb_util:encode(UnsignedID)}) - end. - -unbundle(Item = #tx{data = <>}) -> - {ItemsBin, Items} = decode_bundle_header(Count, Content), - Item#tx{data = decode_bundle_items(Items, ItemsBin)}; -unbundle(#tx{data = <<>>}) -> detached. - -decode_bundle_items([], <<>>) -> - []; -decode_bundle_items([{_ID, Size} | RestItems], ItemsBin) -> - [ - deserialize(binary:part(ItemsBin, 0, Size)) - | - decode_bundle_items( - RestItems, - binary:part( - ItemsBin, - Size, - byte_size(ItemsBin) - Size - ) - ) - ]. - -decode_bundle_header(Count, Bin) -> decode_bundle_header(Count, Bin, []). -decode_bundle_header(0, ItemsBin, Header) -> - {ItemsBin, lists:reverse(Header)}; -decode_bundle_header(Count, <>, Header) -> - decode_bundle_header(Count - 1, Rest, [{ID, Size} | Header]). - -%% @doc Decode the signature from a binary format. Only RSA 4096 is currently supported. -%% Note: the signature type '1' corresponds to RSA 4096 - but it is is written in -%% little-endian format which is why we match on `<<1, 0>>'. -decode_signature(<<1, 0, Signature:512/binary, Owner:512/binary, Rest/binary>>) -> - {{rsa, 65537}, Signature, Owner, Rest}; -decode_signature(Other) -> - ?event({error_decoding_signature, Other}), - unsupported_tx_format. - -%% @doc Decode tags from a binary format using Apache Avro. -decode_tags(<<0:64/little-integer, 0:64/little-integer, Rest/binary>>) -> - {[], Rest}; -decode_tags(<<_TagCount:64/little-integer, _TagSize:64/little-integer, Binary/binary>>) -> - {Count, BlocksBinary} = decode_zigzag(Binary), - {Tags, Rest} = decode_avro_tags(BlocksBinary, Count), - %% Pull out the terminating zero - {0, Rest2} = decode_zigzag(Rest), - {Tags, Rest2}. - -decode_optional_field(<<0, Rest/binary>>) -> - {<<>>, Rest}; -decode_optional_field(<<1:8/integer, Field:32/binary, Rest/binary>>) -> - {Field, Rest}. - -%% @doc Decode Avro blocks (for tags) from binary. -decode_avro_tags(<<>>, _) -> - {[], <<>>}; -decode_avro_tags(Binary, Count) when Count =:= 0 -> - {[], Binary}; -decode_avro_tags(Binary, Count) -> - {NameSize, Rest} = decode_zigzag(Binary), - decode_avro_name(NameSize, Rest, Count). - -decode_avro_name(0, Rest, _) -> - {[], Rest}; -decode_avro_name(NameSize, Rest, Count) -> - <> = Rest, - {ValueSize, Rest3} = decode_zigzag(Rest2), - decode_avro_value(ValueSize, Name, Rest3, Count). - -decode_avro_value(0, Name, Rest, Count) -> - {DecodedTags, NonAvroRest} = decode_avro_tags(Rest, Count - 1), - {[{Name, <<>>} | DecodedTags], NonAvroRest}; -decode_avro_value(ValueSize, Name, Rest, Count) -> - <> = Rest, - {DecodedTags, NonAvroRest} = decode_avro_tags(Rest2, Count - 1), - {[{Name, Value} | DecodedTags], NonAvroRest}. - -%% @doc Decode a VInt encoded ZigZag integer from binary. -decode_zigzag(Binary) -> - {ZigZag, Rest} = decode_vint(Binary, 0, 0), - case ZigZag band 1 of - 1 -> {-(ZigZag bsr 1) - 1, Rest}; - 0 -> {ZigZag bsr 1, Rest} - end. - -decode_vint(<<>>, Result, _Shift) -> - {Result, <<>>}; -decode_vint(<>, Result, Shift) -> - VIntPart = Byte band 16#7F, - NewResult = Result bor (VIntPart bsl Shift), - case Byte band 16#80 of - 0 -> {NewResult, Rest}; - _ -> decode_vint(Rest, NewResult, Shift + 7) - end. - -%%%=================================================================== -%%% Unit tests. -%%% To run: -%%% erlc -o ebin src/*.erl; erl -pa ebin -eval "eunit:test(ar_bundles, [verbose])" -s init stop -%%%=================================================================== - -ar_bundles_test_() -> - [ - {timeout, 30, fun test_no_tags/0}, - {timeout, 30, fun test_with_tags/0}, - {timeout, 30, fun test_with_zero_length_tag/0}, - {timeout, 30, fun test_unsigned_data_item_id/0}, - {timeout, 30, fun test_unsigned_data_item_normalization/0}, - {timeout, 30, fun test_empty_bundle/0}, - {timeout, 30, fun test_bundle_with_one_item/0}, - {timeout, 30, fun test_bundle_with_two_items/0}, - {timeout, 30, fun test_recursive_bundle/0}, - {timeout, 30, fun test_bundle_map/0}, - {timeout, 30, fun test_basic_member_id/0}, - {timeout, 30, fun test_deep_member/0}, - {timeout, 30, fun test_extremely_large_bundle/0}, - {timeout, 30, fun test_serialize_deserialize_deep_signed_bundle/0} - ]. - -run_test() -> - test_with_zero_length_tag(). - -test_no_tags() -> - {Priv, Pub} = ar_wallet:new(), - {KeyType, Owner} = Pub, - Target = crypto:strong_rand_bytes(32), - Anchor = crypto:strong_rand_bytes(32), - DataItem = new_item(Target, Anchor, [], <<"data">>), - SignedDataItem = sign_item(DataItem, {Priv, Pub}), - - ?assertEqual(true, verify_item(SignedDataItem)), - assert_data_item(KeyType, Owner, Target, Anchor, [], <<"data">>, SignedDataItem), - - SignedDataItem2 = deserialize(serialize(SignedDataItem)), - - ?assertEqual(SignedDataItem, SignedDataItem2), - ?assertEqual(true, verify_item(SignedDataItem2)), - assert_data_item(KeyType, Owner, Target, Anchor, [], <<"data">>, SignedDataItem2). - -test_with_tags() -> - {Priv, Pub} = ar_wallet:new(), - {KeyType, Owner} = Pub, - Target = crypto:strong_rand_bytes(32), - Anchor = crypto:strong_rand_bytes(32), - Tags = [{<<"tag1">>, <<"value1">>}, {<<"tag2">>, <<"value2">>}], - DataItem = new_item(Target, Anchor, Tags, <<"taggeddata">>), - SignedDataItem = sign_item(DataItem, {Priv, Pub}), - - ?assertEqual(true, verify_item(SignedDataItem)), - assert_data_item(KeyType, Owner, Target, Anchor, Tags, <<"taggeddata">>, SignedDataItem), - - SignedDataItem2 = deserialize(serialize(SignedDataItem)), - - ?assertEqual(SignedDataItem, SignedDataItem2), - ?assertEqual(true, verify_item(SignedDataItem2)), - assert_data_item(KeyType, Owner, Target, Anchor, Tags, <<"taggeddata">>, SignedDataItem2). - -test_with_zero_length_tag() -> - Item = normalize(#tx{ - format = ans104, - tags = [ - {<<"normal-tag-1">>, <<"tag1">>}, - {<<"empty-tag">>, <<>>}, - {<<"normal-tag-2">>, <<"tag2">>} - ], - data = <<"Typical data field.">> - }), - Serialized = serialize(Item), - Deserialized = deserialize(Serialized), - ?assertEqual(Item, Deserialized). - -test_unsigned_data_item_id() -> - Item1 = deserialize( - serialize(reset_ids(#tx{format = ans104, data = <<"data1">>})) - ), - Item2 = deserialize( - serialize(reset_ids(#tx{format = ans104, data = <<"data2">>}))), - ?assertNotEqual(Item1#tx.unsigned_id, Item2#tx.unsigned_id). - -test_unsigned_data_item_normalization() -> - NewItem = normalize(#tx{ format = ans104, data = <<"Unsigned data">> }), - ReNormItem = deserialize(serialize(NewItem)), - ?assertEqual(NewItem, ReNormItem). - -assert_data_item(KeyType, Owner, Target, Anchor, Tags, Data, DataItem) -> - ?assertEqual(KeyType, DataItem#tx.signature_type), - ?assertEqual(Owner, DataItem#tx.owner), - ?assertEqual(Target, DataItem#tx.target), - ?assertEqual(Anchor, DataItem#tx.last_tx), - ?assertEqual(Tags, DataItem#tx.tags), - ?assertEqual(Data, DataItem#tx.data), - ?assertEqual(byte_size(Data), DataItem#tx.data_size). - -test_empty_bundle() -> - Bundle = serialize([]), - BundleItem = deserialize(Bundle), - ?assertEqual(#{}, BundleItem#tx.data). - -test_bundle_with_one_item() -> - Item = new_item( - crypto:strong_rand_bytes(32), - crypto:strong_rand_bytes(32), - [], - ItemData = crypto:strong_rand_bytes(1000) - ), - ?event({item, Item}), - Bundle = serialize([Item]), - ?event({bundle, Bundle}), - BundleItem = deserialize(Bundle), - ?event({bundle_item, BundleItem}), - ?assertEqual(ItemData, (maps:get(<<"1">>, BundleItem#tx.data))#tx.data). - -test_bundle_with_two_items() -> - Item1 = new_item( - crypto:strong_rand_bytes(32), - crypto:strong_rand_bytes(32), - [], - ItemData1 = crypto:strong_rand_bytes(32) - ), - Item2 = new_item( - crypto:strong_rand_bytes(32), - crypto:strong_rand_bytes(32), - [{<<"tag1">>, <<"value1">>}, {<<"tag2">>, <<"value2">>}], - ItemData2 = crypto:strong_rand_bytes(32) - ), - Bundle = serialize([Item1, Item2]), - BundleItem = deserialize(Bundle), - ?assertEqual(ItemData1, (maps:get(<<"1">>, BundleItem#tx.data))#tx.data), - ?assertEqual(ItemData2, (maps:get(<<"2">>, BundleItem#tx.data))#tx.data). - -test_recursive_bundle() -> - W = ar_wallet:new(), - Item1 = sign_item(#tx{ - id = crypto:strong_rand_bytes(32), - last_tx = crypto:strong_rand_bytes(32), - data = <<1:256/integer>> - }, W), - Item2 = sign_item(#tx{ - id = crypto:strong_rand_bytes(32), - last_tx = crypto:strong_rand_bytes(32), - data = [Item1] - }, W), - Item3 = sign_item(#tx{ - id = crypto:strong_rand_bytes(32), - last_tx = crypto:strong_rand_bytes(32), - data = [Item2] - }, W), - Bundle = serialize([Item3]), - BundleItem = deserialize(Bundle), - #{<<"1">> := UnbundledItem3} = BundleItem#tx.data, - #{<<"1">> := UnbundledItem2} = UnbundledItem3#tx.data, - #{<<"1">> := UnbundledItem1} = UnbundledItem2#tx.data, - ?assert(verify_item(UnbundledItem1)), - % TODO: Verify bundled lists... - ?assertEqual(Item1#tx.data, UnbundledItem1#tx.data). - -test_bundle_map() -> - W = ar_wallet:new(), - Item1 = sign_item(#tx{ - format = ans104, - data = <<"item1_data">> - }, W), - Item2 = sign_item(#tx{ - format = ans104, - last_tx = crypto:strong_rand_bytes(32), - data = #{<<"key1">> => Item1} - }, W), - Bundle = serialize(Item2), - BundleItem = deserialize(Bundle), - ?assertEqual(Item1#tx.data, (maps:get(<<"key1">>, BundleItem#tx.data))#tx.data), - ?assert(verify_item(BundleItem)). - -test_extremely_large_bundle() -> - W = ar_wallet:new(), - Data = crypto:strong_rand_bytes(100_000_000), - Norm = normalize(#tx { data = #{ <<"key">> => #tx { data = Data } } }), - Signed = sign_item(Norm, W), - Serialized = serialize(Signed), - Deserialized = deserialize(Serialized), - ?assert(verify_item(Deserialized)). - -test_basic_member_id() -> - W = ar_wallet:new(), - Item = sign_item( - #tx{ - data = <<"data">> - }, - W - ), - ?assertEqual(true, member(Item#tx.id, Item)), - ?assertEqual(true, member(id(Item, unsigned), Item)), - ?assertEqual(false, member(crypto:strong_rand_bytes(32), Item)). - -test_deep_member() -> - W = ar_wallet:new(), - Item = sign_item( - #tx{ - data = - #{<<"key1">> => - sign_item(#tx{ - data = <<"data">> - }, W) - } - }, - W - ), - Item2 = deserialize(serialize(sign_item( - #tx{ - data = #{ <<"key2">> => Item } - }, - W - ))), - ?assertEqual(true, member(<<"key1">>, Item2)), - ?assertEqual(true, member(<<"key2">>, Item2)), - ?assertEqual(true, member(Item#tx.id, Item2)), - ?assertEqual(true, member(Item2#tx.id, Item2)), - ?assertEqual(true, member(id(Item, unsigned), Item2)), - ?assertEqual(true, member(id(Item2, unsigned), Item2)), - ?assertEqual(false, member(crypto:strong_rand_bytes(32), Item2)). - -test_serialize_deserialize_deep_signed_bundle() -> - W = ar_wallet:new(), - % Test that we can serialize, deserialize, and get the same IDs back. - Item1 = sign_item(#tx{data = <<"item1_data">>}, W), - Item2 = sign_item(#tx{data = #{<<"key1">> => Item1}}, W), - Bundle = serialize(Item2), - Deser2 = deserialize(Bundle), - format(Deser2), - #{ <<"key1">> := Deser1 } = Deser2#tx.data, - format(Deser1), - ?assertEqual(id(Item2, unsigned), id(Deser2, unsigned)), - ?assertEqual(id(Item2, signed), id(Deser2, signed)), - ?assertEqual(id(Item1, unsigned), id(Deser1, unsigned)), - ?assertEqual(id(Item1, signed), id(Deser1, signed)), - % Test that we can sign an item twice and the unsigned ID is the same. - Item3 = sign_item(Item2, W), - ?assertEqual(id(Item3, unsigned), id(Item2, unsigned)), - ?assert(verify_item(Item3)). \ No newline at end of file diff --git a/src/ar_rate_limiter.erl b/src/ar_rate_limiter.erl deleted file mode 100644 index 2fc501912..000000000 --- a/src/ar_rate_limiter.erl +++ /dev/null @@ -1,139 +0,0 @@ --module(ar_rate_limiter). --behaviour(gen_server). --export([start_link/1, throttle/3, off/0, on/0]). --export([init/1, handle_cast/2, handle_call/3, handle_info/2, terminate/2]). --include("include/hb.hrl"). --record(state, { - traces, - off, - opts -}). - -%%%=================================================================== -%%% Public interface. -%%%=================================================================== - -start_link(Opts) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, Opts, []). - -%% @doc Hang until it is safe to make another request to the given Peer with the -%% given Path. The limits are configured in include/ar_blacklist_middleware.hrl. -throttle(Peer, Path, Opts) -> - case lists:member(Peer, hb_opts:get(throttle_exempt_peers, [], Opts)) of - true -> - ok; - false -> - throttle2(Peer, Path, Opts) - end. - -throttle2(Peer, Path, Opts) -> - Routes = hb_opts:get(throttle_exempt_paths, [], Opts), - IsExempt = - lists:any(fun(Route) -> hb_path:regex_matches(Path, Route) end, Routes), - case IsExempt of - true -> ok; - false -> - Res = catch gen_server:call(?MODULE, {throttle, Peer, Path}, infinity), - case Res of - {'EXIT', {noproc, {gen_server, call, _}}} -> - ok; - {'EXIT', Reason} -> - exit(Reason); - _ -> - ok - end - end. - -%% @doc Turn rate limiting off. -off() -> - gen_server:cast(?MODULE, turn_off). - -%% @doc Turn rate limiting on. -on() -> - gen_server:cast(?MODULE, turn_on). - -%%%=================================================================== -%%% Generic server callbacks. -%%%=================================================================== - -init(Opts) -> - process_flag(trap_exit, true), - {ok, #state{ traces = #{}, off = false, opts = Opts }}. - -handle_call({throttle, _Peer, _Path}, _From, #state{ off = true } = State) -> - {reply, ok, State}; -handle_call({throttle, Peer, Path}, From, State) -> - gen_server:cast(?MODULE, {throttle, Peer, Path, From}), - {noreply, State}; - -handle_call(Request, _From, State) -> - ?event(warning, {unhandled_call, {module, ?MODULE}, {request, Request}}), - {reply, ok, State}. - -handle_cast({throttle, Peer, Path, From}, State) -> - #state{ traces = Traces, opts = Opts } = State, - {Type, Limit} = hb_opts:get(throttle_rpm_by_path, Path, Opts), - Now = os:system_time(millisecond), - case maps:get({Peer, Type}, Traces, not_found) of - not_found -> - gen_server:reply(From, ok), - Traces2 = maps:put({Peer, Type}, {1, queue:from_list([Now])}, Traces), - {noreply, State#state{ traces = Traces2 }}; - {N, Trace} -> - {N2, Trace2} = cut_trace(N, queue:in(Now, Trace), Now, Opts), - %% The macro specifies requests per minute while the throttling window - %% is 30 seconds. - HalfLimit = Limit div 2, - %% Try to approach but not hit the limit. - case N2 + 1 > max(1, HalfLimit * 80 / 100) of - true -> - ?event( - {approaching_peer_rpm_limit, - {peer, Peer}, - {path, Path}, - {minute_limit, Limit}, - {caller, From} - } - ), - erlang:send_after( - 1000, - ?MODULE, - {'$gen_cast', {throttle, Peer, Path, From}} - ), - {noreply, State}; - false -> - gen_server:reply(From, ok), - Traces2 = maps:put({Peer, Type}, {N2 + 1, Trace2}, Traces), - {noreply, State#state{ traces = Traces2 }} - end - end; - -handle_cast(turn_off, State) -> - {noreply, State#state{ off = true }}; - -handle_cast(turn_on, State) -> - {noreply, State#state{ off = false }}; - -handle_cast(Cast, State) -> - ?event(warning, {unhandled_cast, {module, ?MODULE}, {cast, Cast}}), - {noreply, State}. - -handle_info(Message, State) -> - ?event(warning, {unhandled_info, {module, ?MODULE}, {message, Message}}), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -%%%=================================================================== -%%% Private functions. -%%%=================================================================== - -cut_trace(N, Trace, Now, Opts) -> - {{value, Timestamp}, Trace2} = queue:out(Trace), - case Timestamp < Now - hb_opts:get(throttle_period, 30000, Opts) of - true -> - cut_trace(N - 1, Trace2, Now, Opts); - false -> - {N, Trace} - end. diff --git a/src/ar_tx.erl b/src/ar_tx.erl deleted file mode 100644 index befb58060..000000000 --- a/src/ar_tx.erl +++ /dev/null @@ -1,209 +0,0 @@ -%%% @doc The module with utilities for transaction creation, signing, and verification. --module(ar_tx). - --export([new/4, new/5, sign/2, verify/1, verify_tx_id/2]). --export([json_struct_to_tx/1, tx_to_json_struct/1]). - --include("include/ar.hrl"). - -%%%=================================================================== -%%% Public interface. -%%%=================================================================== - -%% @doc Create a new transaction. -new(Dest, Reward, Qty, Last) -> - #tx{ - id = crypto:strong_rand_bytes(32), - last_tx = Last, - quantity = Qty, - target = Dest, - data = <<>>, - data_size = 0, - reward = Reward - }. - -new(Dest, Reward, Qty, Last, SigType) -> - #tx{ - id = crypto:strong_rand_bytes(32), - last_tx = Last, - quantity = Qty, - target = Dest, - data = <<>>, - data_size = 0, - reward = Reward, - signature_type = SigType - }. - -%% @doc Cryptographically sign (claim ownership of) a transaction. -sign(TX, {PrivKey, {KeyType, Owner}}) -> - NewTX = TX#tx{ owner = Owner, signature_type = KeyType }, - Sig = ar_wallet:sign(PrivKey, signature_data_segment(NewTX)), - ID = crypto:hash(sha256, <>), - NewTX#tx{ id = ID, signature = Sig }. - -%% @doc Verify whether a transaction is valid. -verify(TX) -> - do_verify(TX, verify_signature). - -%% @doc Verify the given transaction actually has the given identifier. -verify_tx_id(ExpectedID, #tx{ id = ID } = TX) -> - ExpectedID == ID andalso verify_signature(TX, verify_signature) andalso verify_hash(TX). - -%%%=================================================================== -%%% Private functions. -%%%=================================================================== - -%% @doc Generate the data segment to be signed for a given TX. -signature_data_segment(TX) -> - List = [ - << (integer_to_binary(TX#tx.format))/binary >>, - << (TX#tx.owner)/binary >>, - << (TX#tx.target)/binary >>, - << (list_to_binary(integer_to_list(TX#tx.quantity)))/binary >>, - << (list_to_binary(integer_to_list(TX#tx.reward)))/binary >>, - << (TX#tx.last_tx)/binary >>, - << (integer_to_binary(TX#tx.data_size))/binary >>, - << (TX#tx.data_root)/binary >> - ], - ar_deep_hash:hash(List). - -%% @doc Verify the transaction's signature. -verify_signature(TX = #tx{ signature_type = SigType }, verify_signature) -> - SignatureDataSegment = signature_data_segment(TX), - ar_wallet:verify({SigType, TX#tx.owner}, SignatureDataSegment, TX#tx.signature). - -%% @doc Verify that the transaction's ID is a hash of its signature. -verify_hash(#tx{ signature = Sig, id = ID }) -> - ID == crypto:hash(sha256, << Sig/binary >>). - -%% @doc Verify transaction. -do_verify(TX, VerifySignature) -> - From = ar_wallet:to_address(TX#tx.owner, TX#tx.signature_type), - Checks = [ - {"quantity_negative", TX#tx.quantity >= 0}, - {"same_owner_as_target", (From =/= TX#tx.target)}, - {"tx_id_not_valid", verify_hash(TX)}, - {"tx_signature_not_valid", verify_signature(TX, VerifySignature)}, - {"tx_data_size_negative", TX#tx.data_size >= 0}, - {"tx_data_size_data_root_mismatch", (TX#tx.data_size == 0) == (TX#tx.data_root == <<>>)} - ], - collect_validation_results(TX#tx.id, Checks). - -collect_validation_results(_TXID, Checks) -> - KeepFailed = fun - ({_, true}) -> false; - ({ErrorCode, false}) -> {true, ErrorCode} - end, - case lists:filtermap(KeepFailed, Checks) of - [] -> true; - _ -> false - end. - -json_struct_to_tx(TXStruct) -> - Tags = - case hb_util:find_value(<<"tags">>, TXStruct) of - undefined -> - []; - Xs -> - Xs - end, - Data = hb_util:decode(hb_util:find_value(<<"data">>, TXStruct)), - Format = - case hb_util:find_value(<<"format">>, TXStruct) of - undefined -> - 1; - N when is_integer(N) -> - N; - N when is_binary(N) -> - binary_to_integer(N) - end, - Denomination = - case hb_util:find_value(<<"denomination">>, TXStruct) of - undefined -> - 0; - EncodedDenomination -> - MaybeDenomination = binary_to_integer(EncodedDenomination), - true = MaybeDenomination > 0, - MaybeDenomination - end, - TXID = hb_util:decode(hb_util:find_value(<<"id">>, TXStruct)), - 32 = byte_size(TXID), - #tx{ - format = Format, - id = TXID, - last_tx = hb_util:decode(hb_util:find_value(<<"last_tx">>, TXStruct)), - owner = hb_util:decode(hb_util:find_value(<<"owner">>, TXStruct)), - tags = [{hb_util:decode(Name), hb_util:decode(Value)} - %% Only the elements matching this pattern are included in the list. - || {[{<<"name">>, Name}, {<<"value">>, Value}]} <- Tags], - target = hb_util:find_value(<<"target">>, TXStruct), - quantity = binary_to_integer(hb_util:find_value(<<"quantity">>, TXStruct)), - data = Data, - reward = binary_to_integer(hb_util:find_value(<<"reward">>, TXStruct)), - signature = hb_util:decode(hb_util:find_value(<<"signature">>, TXStruct)), - data_size = binary_to_integer(hb_util:find_value(<<"data_size">>, TXStruct)), - data_root = - case hb_util:find_value(<<"data_root">>, TXStruct) of - undefined -> <<>>; - DR -> hb_util:decode(DR) - end, - denomination = Denomination - }. - -tx_to_json_struct( - #tx{ - id = ID, - format = Format, - last_tx = Last, - owner = Owner, - tags = Tags, - target = Target, - quantity = Quantity, - data = Data, - reward = Reward, - signature = Sig, - data_size = DataSize, - data_root = DataRoot, - denomination = Denomination - }) -> - Fields = [ - {format, - case Format of - undefined -> - 1; - _ -> - Format - end}, - {id, hb_util:encode(ID)}, - {last_tx, hb_util:encode(Last)}, - {owner, hb_util:encode(Owner)}, - {tags, - lists:map( - fun({Name, Value}) -> - { - [ - {name, hb_util:encode(Name)}, - {value, hb_util:encode(Value)} - ] - } - end, - Tags - ) - }, - {target, hb_util:encode(Target)}, - {quantity, integer_to_binary(Quantity)}, - {data, hb_util:encode(Data)}, - {data_size, integer_to_binary(DataSize)}, - {data_tree, []}, - {data_root, hb_util:encode(DataRoot)}, - {reward, integer_to_binary(Reward)}, - {signature, hb_util:encode(Sig)} - ], - Fields2 = - case Denomination > 0 of - true -> - Fields ++ [{denomination, integer_to_binary(Denomination)}]; - false -> - Fields - end, - maps:from_list(Fields2). \ No newline at end of file diff --git a/src/ar_wallet.erl b/src/ar_wallet.erl deleted file mode 100644 index bf4304bec..000000000 --- a/src/ar_wallet.erl +++ /dev/null @@ -1,222 +0,0 @@ --module(ar_wallet). --export([sign/2, sign/3, hmac/1, hmac/2, verify/3, verify/4, to_address/1, to_address/2, new/0, new/1]). --export([new_keyfile/2, load_keyfile/1, load_key/1]). --include("include/ar.hrl"). --include_lib("public_key/include/public_key.hrl"). - -%%% @doc Utilities for manipulating wallets. - --define(WALLET_DIR, "."). - -%%% Public interface. - -new() -> - new({rsa, 65537}). -new(KeyType = {KeyAlg, PublicExpnt}) when KeyType =:= {rsa, 65537} -> - {[_, Pub], [_, Pub, Priv|_]} = {[_, Pub], [_, Pub, Priv|_]} - = crypto:generate_key(KeyAlg, {4096, PublicExpnt}), - {{KeyType, Priv, Pub}, {KeyType, Pub}}. - -%% @doc Sign some data with a private key. -sign(Key, Data) -> - sign(Key, Data, sha256). - -%% @doc sign some data, hashed using the provided DigestType. -%% TODO: support signing for other key types -sign({{rsa, PublicExpnt}, Priv, Pub}, Data, DigestType) when PublicExpnt =:= 65537 -> - rsa_pss:sign( - Data, - DigestType, - #'RSAPrivateKey'{ - publicExponent = PublicExpnt, - modulus = binary:decode_unsigned(Pub), - privateExponent = binary:decode_unsigned(Priv) - } - ). - -hmac(Data) -> - hmac(Data, sha256). - -hmac(Data, DigestType) -> crypto:mac(hmac, DigestType, <<"ar">>, Data). - -%% @doc Verify that a signature is correct. -verify(Key, Data, Sig) -> - verify(Key, Data, Sig, sha256). - -verify({{rsa, PublicExpnt}, Pub}, Data, Sig, DigestType) when PublicExpnt =:= 65537 -> - rsa_pss:verify( - Data, - DigestType, - Sig, - #'RSAPublicKey'{ - publicExponent = PublicExpnt, - modulus = binary:decode_unsigned(Pub) - } - ). - -%% @doc Generate an address from a public key. -to_address(Pubkey) -> - to_address(Pubkey, ?DEFAULT_KEY_TYPE). -to_address(PubKey, _) when bit_size(PubKey) == 256 -> - %% Small keys are not secure, nobody is using them, the clause - %% is for backwards-compatibility. - PubKey; -to_address({{_, _, PubKey}, {_, PubKey}}, _) -> - to_address(PubKey); -to_address(PubKey, {rsa, 65537}) -> - to_rsa_address(PubKey); -to_address(PubKey, {ecdsa, 256}) -> - to_ecdsa_address(PubKey). - -%% @doc Generate a new wallet public and private key, with a corresponding keyfile. -%% The provided key is used as part of the file name. -new_keyfile(KeyType, WalletName) when is_list(WalletName) -> - new_keyfile(KeyType, list_to_binary(WalletName)); -new_keyfile(KeyType, WalletName) -> - {Pub, Priv, Key} = - case KeyType of - {?RSA_SIGN_ALG, PublicExpnt} -> - {[Expnt, Pb], [Expnt, Pb, Prv, P1, P2, E1, E2, C]} = - crypto:generate_key(rsa, {?RSA_PRIV_KEY_SZ, PublicExpnt}), - Ky = - hb_json:encode( - #{ - kty => <<"RSA">>, - ext => true, - e => hb_util:encode(Expnt), - n => hb_util:encode(Pb), - d => hb_util:encode(Prv), - p => hb_util:encode(P1), - q => hb_util:encode(P2), - dp => hb_util:encode(E1), - dq => hb_util:encode(E2), - qi => hb_util:encode(C) - } - ), - {Pb, Prv, Ky}; - {?ECDSA_SIGN_ALG, secp256k1} -> - {OrigPub, Prv} = crypto:generate_key(ecdh, secp256k1), - <<4:8, PubPoint/binary>> = OrigPub, - PubPointMid = byte_size(PubPoint) div 2, - <> = PubPoint, - Ky = - hb_json:encode( - #{ - kty => <<"EC">>, - crv => <<"secp256k1">>, - x => hb_util:encode(X), - y => hb_util:encode(Y), - d => hb_util:encode(Prv) - } - ), - {compress_ecdsa_pubkey(OrigPub), Prv, Ky}; - {?EDDSA_SIGN_ALG, ed25519} -> - {{_, Prv, Pb}, _} = new(KeyType), - Ky = - hb_json:encode( - #{ - kty => <<"OKP">>, - alg => <<"EdDSA">>, - crv => <<"Ed25519">>, - x => hb_util:encode(Pb), - d => hb_util:encode(Prv) - } - ), - {Pb, Prv, Ky} - end, - Filename = wallet_filepath(WalletName, Pub, KeyType), - filelib:ensure_dir(Filename), - file:write_file(Filename, Key), - {{KeyType, Priv, Pub}, {KeyType, Pub}}. - -wallet_filepath(Wallet) -> - filename:join([?WALLET_DIR, binary_to_list(Wallet)]). - -wallet_filepath2(Wallet) -> - filename:join([?WALLET_DIR, binary_to_list(Wallet)]). - -%% @doc Read the keyfile for the key with the given address from disk. -%% Return not_found if arweave_keyfile_[addr].json or [addr].json is not found -%% in [data_dir]/?WALLET_DIR. -load_key(Addr) -> - Path = hb_util:encode(Addr), - case filelib:is_file(Path) of - false -> - Path2 = wallet_filepath2(hb_util:encode(Addr)), - case filelib:is_file(Path2) of - false -> - not_found; - true -> - load_keyfile(Path2) - end; - true -> - load_keyfile(Path) - end. - -%% @doc Extract the public and private key from a keyfile. -load_keyfile(File) -> - {ok, Body} = file:read_file(File), - Key = hb_json:decode(Body), - {Pub, Priv, KeyType} = - case maps:get(<<"kty">>, Key) of - <<"EC">> -> - XEncoded = maps:get(<<"x">>, Key), - YEncoded = maps:get(<<"y">>, Key), - PrivEncoded = maps:get(<<"d">>, Key), - OrigPub = iolist_to_binary([<<4:8>>, hb_util:decode(XEncoded), - hb_util:decode(YEncoded)]), - Pb = compress_ecdsa_pubkey(OrigPub), - Prv = hb_util:decode(PrivEncoded), - KyType = {?ECDSA_SIGN_ALG, secp256k1}, - {Pb, Prv, KyType}; - <<"OKP">> -> - PubEncoded = maps:get(<<"x">>, Key), - PrivEncoded = maps:get(<<"d">>, Key), - Pb = hb_util:decode(PubEncoded), - Prv = hb_util:decode(PrivEncoded), - KyType = {?EDDSA_SIGN_ALG, ed25519}, - {Pb, Prv, KyType}; - _ -> - PubEncoded = maps:get(<<"n">>, Key), - PrivEncoded = maps:get(<<"d">>, Key), - Pb = hb_util:decode(PubEncoded), - Prv = hb_util:decode(PrivEncoded), - KyType = {?RSA_SIGN_ALG, 65537}, - {Pb, Prv, KyType} - end, - {{KeyType, Priv, Pub}, {KeyType, Pub}}. - -%%%=================================================================== -%%% Private functions. -%%%=================================================================== - -to_rsa_address(PubKey) -> - hash_address(PubKey). - -hash_address(PubKey) -> - crypto:hash(sha256, PubKey). - -to_ecdsa_address(PubKey) -> - hb_keccak:key_to_ethereum_address(PubKey). - -%%%=================================================================== -%%% Private functions. -%%%=================================================================== - -wallet_filepath(WalletName, PubKey, KeyType) -> - wallet_filepath(wallet_name(WalletName, PubKey, KeyType)). - -wallet_name(wallet_address, PubKey, KeyType) -> - hb_util:encode(to_address(PubKey, KeyType)); -wallet_name(WalletName, _, _) -> - WalletName. - -compress_ecdsa_pubkey(<<4:8, PubPoint/binary>>) -> - PubPointMid = byte_size(PubPoint) div 2, - <> = PubPoint, - PubKeyHeader = - case Y rem 2 of - 0 -> <<2:8>>; - 1 -> <<3:8>> - end, - iolist_to_binary([PubKeyHeader, X]). \ No newline at end of file diff --git a/src/cargo.hrl b/src/cargo.hrl deleted file mode 100644 index bcbb63656..000000000 --- a/src/cargo.hrl +++ /dev/null @@ -1,8 +0,0 @@ --cargo_header_version 1. --ifndef(CARGO_LOAD_APP). --define(CARGO_LOAD_APP,hb). --endif. --ifndef(CARGO_HRL). --define(CARGO_HRL, 1). --define(load_nif_from_crate(__CRATE,__INIT),(fun()->__APP=?CARGO_LOAD_APP,__PATH=filename:join([code:priv_dir(__APP),"crates",__CRATE,__CRATE]),erlang:load_nif(__PATH,__INIT)end)()). --endif. diff --git a/src/core/device/hb_device.erl b/src/core/device/hb_device.erl new file mode 100644 index 000000000..50291980c --- /dev/null +++ b/src/core/device/hb_device.erl @@ -0,0 +1,301 @@ +%%% @doc A library for working with HyperBEAM-compatible AO-Core devices. +%%% Offers services for loading, verifying executability, and extracting Erlang +%%% functions from a device. +-module(hb_device). +-export([truncate_args/2, message_to_fun/3, message_to_device/2]). +-export([is_direct_key_access/3, is_direct_key_access/4]). +-export([find_exported_function/5, is_exported/4, info/2, info/3]). +-include("include/hb.hrl"). + +-define(DEFAULT_DEVICE, <<"message@1.0">>). + +%%% All keys in the `message@1.0` device that are not resolved to underlying +%%% data in the their Erlang map representations. +-define(MESSAGE_KEYS, [ + <<"get">>, + <<"set">>, + <<"remove">>, + <<"keys">>, + <<"id">>, + <<"commit">>, + <<"verify">>, + <<"committers">>, + <<"committed">> +]). + +%% @doc Truncate the arguments of a function to the number of arguments it +%% actually takes. +truncate_args(Fun, Args) -> + {arity, Arity} = erlang:fun_info(Fun, arity), + lists:sublist(Args, Arity). + +%% @doc Calculate the Erlang function that should be called to get a value for +%% a given key from a device. +%% +%% This comes in 7 forms: +%% 1. The message does not specify a device, so we use the default device. +%% 2. The device has a `handler' key in its `Dev:info()' map, which is a +%% function that takes a key and returns a function to handle that key. We pass +%% the key as an additional argument to this function: +%% `Mod:Handler(Key, Base, Req, Opts) -> {Status, Fun}' +%% 3. The device has a function of the name `Key', which should be called +%% directly. +%% 4. The device does not implement the key, but does have a default function +%% for us to call. We pass it the key as an additional argument, as with (2). +%% `default' differs from `handler' in that it only matches for keys where the +%% module exports no function of the given name. +%% 5. The device has a `default' key with a device or module name as its value. +%% We use this device to handle the key, restarting the process of resolving the +%% key to a function. +%% 6. The device does not implement the key and states no defaults. We use the +%% global default device to handle the key. +%% Error: If the device is specified, but not loadable, we raise an error. +%% +%% Returns {ok | add_key, Fun} where Fun is the function to call, and add_key +%% indicates that the key should be added to the start of the call's arguments. +message_to_fun(Msg, Key, Opts) -> + % Get the device module from the message and recurse. + message_to_fun(message_to_device(Msg, Opts), Msg, Key, Opts). +message_to_fun(Dev, Msg, Key, Opts) -> + Info = info(Dev, Msg, Opts), + % Is the key exported by the device? + Exported = is_exported(Info, Key, Opts), + ?event( + ao_devices, + {message_to_fun, + {dev, Dev}, + {key, Key}, + {is_exported, Exported}, + {opts, Opts} + }, + Opts + ), + % Does the device have an explicit handler function? + case {hb_maps:find(handler, Info, Opts), Exported} of + {{ok, Handler}, true} -> + % Case 2: The device has an explicit handler function. + ?event( + ao_devices, + {handler_found, {dev, Dev}, {key, Key}, {handler, Handler}} + ), + {Status, Func} = info_handler_to_fun(Handler, Msg, Key, Opts), + {Status, Dev, Func}; + _ -> + ?event(ao_devices, {no_override_handler, {dev, Dev}, {key, Key}}), + case {find_exported_function(Msg, Dev, Key, 3, Opts), Exported} of + {{ok, Func}, true} -> + % Case 3: The device has a function of the name `Key'. + {ok, Dev, Func}; + _ -> + case {hb_maps:find(default, Info, Opts), Exported} of + {{ok, DefaultFunc}, true} when is_function(DefaultFunc) -> + % Case 4: The device has a default handler. + ?event({found_default_handler, {func, DefaultFunc}}), + {add_key, Dev, DefaultFunc}; + {{ok, DefaultDevice}, true} when is_binary(DefaultDevice) + orelse is_atom(DefaultDevice) -> + % Case 5: The device gives a specific further device + % to default to. Recurse with it and apply the same + % rules. + ?event({found_default_device, {mod, DefaultDevice}}), + message_to_fun( + Msg#{ <<"device">> => DefaultDevice }, + Key, + Opts + ); + _ -> + % Case 6: The device has no default handler. + % We retry with the default unless the message + % already names it (loop guard). + case hb_maps:get(<<"device">>, Msg, undefined, Opts) of + ?DEFAULT_DEVICE -> + throw({ + error, + default_device_could_not_resolve_key, + {key, Key} + }); + _ -> + ?event({using_default_device, ?DEFAULT_DEVICE}), + message_to_fun( + Msg#{ <<"device">> => ?DEFAULT_DEVICE }, + Key, + Opts + ) + end + end + end + end. + +%% @doc Extract the runtime device module from a message. When the +%% message has no `<<"device">>' key, we resolve the default +%% (`message@1.0') just like any other device: There is no privileged +%% internal module-loading path. +message_to_device(Msg, Opts) -> + DevID = hb_maps:get(<<"device">>, Msg, ?DEFAULT_DEVICE, Opts), + case hb_device_load:reference(DevID, Opts) of + {error, Reason} -> throw({error, {device_not_loadable, DevID, Reason}}); + {ok, DevMod} -> DevMod + end. + +%% @doc Parse a handler key given by a device's `info'. +info_handler_to_fun(Handler, _Msg, _Key, _Opts) when is_function(Handler) -> + {add_key, Handler}; +info_handler_to_fun(HandlerMap, Msg, Key, Opts) -> + case hb_maps:find(excludes, HandlerMap, Opts) of + {ok, Exclude} -> + case lists:member(Key, Exclude) of + true -> + MsgWithoutDevice = + hb_maps:without([<<"device">>], Msg, Opts), + message_to_fun( + MsgWithoutDevice#{ <<"device">> => ?DEFAULT_DEVICE }, + Key, + Opts + ); + false -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)} + end; + error -> {add_key, hb_maps:get(func, HandlerMap, undefined, Opts)} + end. + +%% @doc Find the function with the highest arity that has the given name, if it +%% exists. +%% +%% If the device is a module, we look for a function with the given name. +%% +%% If the device is a map, we look for a key in the map. First we try to find +%% the key using its literal value. If that fails, we cast the key to an atom +%% and try again. +find_exported_function(Msg, Mod, Key, Arity, Opts) when not is_atom(Key) -> + try hb_util:key_to_atom(Key, false) of + KeyAtom -> find_exported_function(Msg, Mod, KeyAtom, Arity, Opts) + catch _:_ -> not_found + end; +find_exported_function(Msg, Dev, Key, MaxArity, Opts) when is_map(Dev) -> + NormKey = hb_ao:normalize_key(Key), + NormDev = hb_ao:normalize_keys(Dev, Opts), + case hb_maps:get(NormKey, NormDev, not_found, Opts) of + not_found -> not_found; + Fun when is_function(Fun) -> + case erlang:fun_info(Fun, arity) of + {arity, Arity} when Arity =< MaxArity -> + case is_exported(Msg, Dev, Key, Opts) of + true -> {ok, Fun}; + false -> not_found + end; + _ -> not_found + end + end; +find_exported_function(_Msg, _Mod, _Key, Arity, _Opts) when Arity < 0 -> + not_found; +find_exported_function(Msg, Mod, Key, Arity, Opts) -> + case erlang:function_exported(Mod, Key, Arity) of + true -> + case is_exported(Msg, Mod, Key, Opts) of + true -> {ok, fun Mod:Key/Arity}; + false -> not_found + end; + false -> + find_exported_function(Msg, Mod, Key, Arity - 1, Opts) + end. + +%% @doc Check if a device is guarding a key via its `exports' list. Defaults to +%% true if the device does not specify an `exports' list. The `info' function is +%% always exported, if it exists. Elements of the `exludes' list are not +%% exported. Note that we check for info _twice_ -- once when the device is +%% given but the info result is not, and once when the info result is given. +%% The reason for this is that `info/3' calls other functions that may need to +%% check if a key is exported, so we must avoid infinite loops. We must, however, +%% also return a consistent result in the case that only the info result is +%% given, so we check for it in both cases. +is_exported(_Msg, _Dev, info, _Opts) -> true; +is_exported(Msg, Dev, Key, Opts) -> + is_exported(info(Dev, Msg, Opts), Key, Opts). +is_exported(_, info, _Opts) -> true; +is_exported(Info = #{ excludes := Excludes }, Key, Opts) -> + NormKey = maybe_normalize_device_key(Key, existing), + case lists:member(NormKey, lists:map(fun maybe_normalize_device_key/1, Excludes)) of + true -> false; + false -> is_exported(hb_maps:remove(excludes, Info, Opts), Key, Opts) + end; +is_exported(#{ exports := Exports }, Key, _Opts) -> + lists:member( + maybe_normalize_device_key(Key, existing), + lists:map(fun maybe_normalize_device_key/1, Exports) + ); +is_exported(_Info, _Key, _Opts) -> true. + +%% @doc Normalize an exported key to its canonical atomized form. By default +%% new atoms are created if necessary. In practice this is used for keys that +%% orinate from a device's `info' response, but _not_ for keys that could be +%% chosen by non-author users. This imparts a requirement that device developers +%% should not generate too many different exports/excludes -- just as they should +%% not generate too many atoms. +maybe_normalize_device_key(Key) -> maybe_normalize_device_key(Key, new_atoms). +maybe_normalize_device_key(Key, Mode) -> + try hb_util:key_to_atom(hb_ao:normalize_key(Key), Mode) + catch _:_ -> Key + end. + +%% @doc Get the info map for a device, optionally giving it a message if the +%% device's info function is parameterized by one. +info(Msg, Opts) -> info(message_to_device(Msg, Opts), Msg, Opts). +info(DevMod, Msg, Opts) -> + case find_exported_function(Msg, DevMod, info, 2, Opts) of + {ok, Fun} -> apply(Fun, truncate_args(Fun, [Msg, Opts])); + not_found -> #{} + end. + +%% @doc Determine if a device is a `direct access': If there is a literal key +%% in the message's Erlang map representation, will it always be returned? +is_direct_key_access(Base, Req, Opts) -> + is_direct_key_access(Base, Req, Opts, unknown). +is_direct_key_access(Base, Req, Opts, MaybeStore) when ?IS_ID(Base) -> + Store = + if MaybeStore =:= unknown -> hb_opts:get(store, no_viable_store, Opts); + true -> MaybeStore + end, + DevPath = + hb_util:ok_or( + hb_store:resolve(Store, [Base, <<"device">>], Opts), + [Base, <<"device">>] + ), + case hb_store:read(Store, DevPath, Opts) of + {ok, Dev} -> + do_is_direct_key_access(Dev, Req, Opts); + {error, not_found} -> + fallback_direct_key_access(Store, Base, Req, Opts) + end; +is_direct_key_access(Base, Req, Opts, _) when is_map(Base) -> + do_is_direct_key_access(hb_maps:find(<<"device">>, Base, Opts), Req, Opts). + +fallback_direct_key_access(Store, Base, Req, Opts) -> + case hb_store:type(Store, Base, Opts) of + {error, not_found} -> unknown; + {ok, _} -> do_is_direct_key_access(<<"message@1.0">>, Req, Opts) + end. + +do_is_direct_key_access(DevRes, #{ <<"path">> := Key }, Opts) -> + do_is_direct_key_access(DevRes, Key, Opts); +do_is_direct_key_access({_Status, DevRes}, Key, Opts) -> + do_is_direct_key_access(DevRes, Key, Opts); +do_is_direct_key_access(not_found, Key, Opts) -> + do_is_direct_key_access(<<"message@1.0">>, Key, Opts); +do_is_direct_key_access(error, Key, Opts) -> + do_is_direct_key_access(<<"message@1.0">>, Key, Opts); +do_is_direct_key_access(<<"message@1.0">>, Key, _Opts) -> + not lists:member(Key, ?MESSAGE_KEYS); +do_is_direct_key_access(Dev, NormKey, Opts) -> + ?event(debug_read_cached, {calculating_info, {device, Dev}}), + case info(#{ <<"device">> => Dev}, Opts) of + Info = #{ exports := Exports } + when not is_map_key(handler, Info) andalso not is_map_key(default, Info) -> + ?event(debug_read_cached, + {exports, + {device, Dev}, + {key, NormKey}, + {exports, Exports} + } + ), + not lists:member(NormKey, Exports ++ ?MESSAGE_KEYS); + _ -> false + end. diff --git a/src/core/device/hb_device_archive.erl b/src/core/device/hb_device_archive.erl new file mode 100644 index 000000000..350e4d36b --- /dev/null +++ b/src/core/device/hb_device_archive.erl @@ -0,0 +1,352 @@ +%%% @doc Helpers for packaged-device implementation archives. +-module(hb_device_archive). +-export([create/2, module_metadata/1, contents/1]). +-export([load/1, load/4, load_modules/1, loaded/1]). +-export([implementation_dir/1]). +-export([modules_match_root/2, write_resources/2]). +-include_lib("kernel/include/file.hrl"). + +-define(DEFAULT_IMPLEMENTATION_DIR, "_build/device-implementations"). + +%% @doc Create the deterministic in-memory ZIP used as implementation body. +create(Compiled, PrivFiles) -> + Files = beam_files(Compiled) ++ resource_files(PrivFiles), + {ok, {_, Archive}} = + zip:create( + <<"device.beams.zip">>, + Files, + [memory, {extra, []}, {uncompress, all}] + ), + Archive. + +%% @doc Build flat archive metadata for generated BEAM modules. +module_metadata(Compiled) -> + [ + #{ + <<"module-name">> => atom_to_binary(Mod, utf8), + <<"archive-path">> => archive_path(Mod) + } + || + #{ module := Mod } <- Compiled + ]. + +%% @doc Extract loadable modules and resources from an implementation archive. +contents(Archive) -> + case zip:unzip(Archive, [memory]) of + {ok, Files} -> read_entries(Files, [], []); + {error, Reason} -> {error, {archive_extract_failed, Reason}} + end. + +%% @doc Return deterministic archive entries for compiled BEAM modules. +beam_files(Compiled) -> + [ + { + hb_util:list(archive_path(Mod)), + Beam, + archive_file_info(<<"ebin">>, byte_size(Beam)) + } + || + #{ module := Mod, beam := Beam } <- Compiled + ]. + +%% @doc Return deterministic archive entries for priv resources. +resource_files(PrivFiles) -> + [ + {hb_util:list(Path), Body, archive_file_info(Path, byte_size(Body))} + || + {Path, Body} <- lists:sort(maps:to_list(PrivFiles)) + ]. + +%% @doc Return the archive path for a BEAM module. +archive_path(Mod) -> + filename:join( + <<"ebin">>, + <<(atom_to_binary(Mod, utf8))/binary, ".beam">> + ). + +%% @doc Return deterministic zip file metadata for reproducible archives. +archive_file_info(Path, Size) -> + FixedTime = {{1980, 1, 1}, {0, 0, 0}}, + #file_info{ + size = Size, + type = regular, + access = read, + atime = FixedTime, + mtime = FixedTime, + ctime = FixedTime, + mode = archive_file_mode(Path) + }. + +%% @doc Mark executable archive resources when their path convention implies it. +archive_file_mode(<<"priv/bin/", _/binary>>) -> 8#100755; +archive_file_mode(Path) -> + case filename:extension(hb_util:list(Path)) of + ".sh" -> 8#100755; + _ -> 8#100644 + end. + +%% @doc Load every BEAM in an archive into the current code server. +load(Archive) -> + case contents(Archive) of + {ok, Modules, _Resources} -> load_modules(Modules); + {error, _} = Error -> Error + end. + +%% @doc Validate and load a signed implementation archive. +load(undefined, _Archive, _Msg, _Opts) -> + {error, missing_module_name}; +load(_ModBin, undefined, _Msg, _Opts) -> + {error, missing_archive}; +load(ModBin, Archive, _Msg, Opts) -> + maybe + {ok, Root} ?= generated_module(ModBin), + {ok, Modules, Resources} ?= contents(Archive), + ok ?= modules_match_root(Root, Modules), + load_new_archive(Root, ModBin, Modules, Resources, Opts) + end. + +%% @doc Parse archive entries into BEAM modules and priv resources. +read_entries([], ModulesAcc, ResourceAcc) -> + Modules = [Mod || {Mod, _, _} <- ModulesAcc], + Resources = [Path || {Path, _} <- ResourceAcc], + case { + length(Modules) =:= length(lists:usort(Modules)), + length(Resources) =:= length(lists:usort(Resources)) + } of + {true, true} -> + {ok, lists:reverse(ModulesAcc), lists:reverse(ResourceAcc)}; + {false, _} -> {error, duplicate_archive_module}; + {_, false} -> {error, duplicate_archive_file} + end; +read_entries([{Path0, Body} | Rest], ModulesAcc, ResourceAcc) -> + Path = hb_util:bin(Path0), + case Path of + <<"ebin/", _/binary>> -> + case beam_module(Path, Body) of + {ok, Mod} -> + read_entries( + Rest, + [{Mod, binary_to_list(Path), Body} | ModulesAcc], + ResourceAcc + ); + {error, Reason} -> + {error, Reason} + end; + <<"priv/", Rel/binary>> -> + case safe_resource(Rel) of + ok -> + read_entries(Rest, ModulesAcc, [{Rel, Body} | ResourceAcc]); + {error, Reason} -> {error, Reason} + end; + _ -> + {error, {unsupported_archive_path, Path}} + end. + +%% @doc Return the declared module name of a generated BEAM archive member. +beam_module(Path, Beam) -> + case beam_lib:chunks(Beam, [exports]) of + {ok, {Mod, _Chunks}} -> + ModBin = atom_to_binary(Mod, utf8), + ExpectedPath = <<"ebin/", ModBin/binary, ".beam">>, + case {hb_device_name:is_generated(Mod), Path} of + {false, _} -> {error, {non_generated_module_name, ModBin}}; + {true, ExpectedPath} -> {ok, Mod}; + {true, _} -> + {error, {archive_path_mismatch, Path, ExpectedPath}} + end; + {error, _Module, Reason} -> + {error, {invalid_beam, Path, Reason}} + end. + +%% @doc Ensure every archive module belongs to the root generated namespace. +modules_match_root(RootMod, Modules) -> + case lists:keymember(RootMod, 1, Modules) of + false -> + {error, archive_missing_root}; + true -> + RootBin = atom_to_binary(RootMod, utf8), + Prefix = <>, + case [ + Mod + || + {Mod, _, _} <- Modules, + not same_archive_namespace(Mod, RootBin, Prefix) + ] of + [] -> ok; + Bad -> {error, {archive_module_outside_namespace, Bad}} + end + end. + +%% @doc Return true if a module belongs to the archive root namespace. +same_archive_namespace(Mod, RootBin, Prefix) -> + ModBin = atom_to_binary(Mod, utf8), + ModBin =:= RootBin orelse + binary:match(ModBin, Prefix) =:= {0, byte_size(Prefix)}. + +%% @doc Reject archive resource paths that could escape the target directory. +safe_resource(<<>>) -> + {error, empty_archive_resource_path}; +safe_resource(Rel) -> + Parts = binary:split(Rel, <<"/">>, [global]), + case binary:match(Rel, <<"\\">>) =/= nomatch orelse + lists:any(fun unsafe_resource_part/1, Parts) + of + true -> {error, {unsafe_archive_resource_path, Rel}}; + false -> ok + end. + +%% @doc Return true for path components unsafe inside an archive resource path. +unsafe_resource_part(<<>>) -> true; +unsafe_resource_part(<<".">>) -> true; +unsafe_resource_part(<<"..">>) -> true; +unsafe_resource_part(_) -> false. + +%% @doc Load archive modules. OTP cannot atomically load modules that +%% declare `-on_load', so normal devices use the atomic path while +%% on-load devices fall back to ordinary Erlang loading semantics. +load_modules(Modules) -> + case code:atomic_load(Modules) of + ok -> ok; + {error, Reason} -> + case atomic_load_rejected_on_load(Reason) of + true -> load_modules_naturally(Modules); + false -> + case loaded(Modules) of + true -> ok; + false -> {error, Reason} + end + end + end. + +%% @doc Return true if `code:atomic_load/1' rejected module on-load callbacks. +atomic_load_rejected_on_load(Reason) when is_list(Reason) -> + lists:any( + fun({_Mod, on_load_not_allowed}) -> true; + (_) -> false + end, + Reason + ); +atomic_load_rejected_on_load(_Reason) -> + false. + +%% @doc Load each archive module with ordinary Erlang code loading. +load_modules_naturally(Modules) -> + lists:foldl(fun load_one_module/2, ok, Modules). + +%% @doc Load a single archive module unless it is already loaded. +load_one_module(_Module, {error, _} = Error) -> + Error; +load_one_module({Mod, File, Beam}, ok) -> + case code:is_loaded(Mod) of + false -> + case code:load_binary(Mod, File, Beam) of + {module, Mod} -> ok; + {error, Reason} -> {error, {Mod, Reason}} + end; + _ -> + ok + end. + +%% @doc Check whether every archive module is present in the code server. +loaded(Modules) -> + lists:all(fun({Mod, _, _}) -> code:is_loaded(Mod) =/= false end, Modules). + +%% @doc Return the extracted implementation directory for a generated device. +implementation_dir(Module) when is_atom(Module) -> + Root = hb_device_name:root(Module), + persistent_term:get( + {?MODULE, implementation_dir, Root}, + filename:join(implementation_root(), atom_to_list(Root)) + ). + +%% @doc Convert an implementation message module name to a generated atom. +generated_module(ModBin) -> + case hb_device_name:is_generated(ModBin) of + false -> {error, {non_generated_module_name, ModBin}}; + true -> {ok, hb_util:key_to_atom(ModBin, new_atoms)} + end. + +%% @doc Prepare archive resources before module load. +prepare_implementation_dir(_RootMod, _ImplementationID, [], _Opts) -> + ok; +prepare_implementation_dir(RootMod, ImplementationID, Files, Opts) -> + Root = hb_device_name:root(RootMod), + Dir = + filename:join( + implementation_root(Opts), + hb_util:list(ImplementationID) + ), + case write_resources(Dir, Files) of + ok -> + persistent_term:put({?MODULE, implementation_dir, Root}, Dir), + ok; + {error, _} = Error -> + Error + end. + +%% @doc Load archive contents unless every module is already in memory. +load_new_archive(Root, ModBin, Modules, Resources, Opts) -> + case loaded(Modules) of + true -> {ok, Root}; + false -> global:trans({?MODULE, Root}, fun() -> + case loaded(Modules) of + true -> + {ok, Root}; + false -> + maybe + ok ?= prepare_implementation_dir( + Root, + ModBin, + Resources, + Opts + ), + ok ?= load_modules(Modules), + {ok, Root} + end + end + end) + end. + +%% @doc Return the default implementation resource root. +implementation_root() -> + case os:getenv("HB_DEVICE_IMPLEMENTATION_DIR") of + false -> filename:absname(?DEFAULT_IMPLEMENTATION_DIR); + Dir -> Dir + end. + +%% @doc Return the configured implementation resource root. +implementation_root(Opts) -> + hb_util:list( + hb_opts:get( + <<"device-implementation-dir">>, + implementation_root(), + Opts + ) + ). + +%% @doc Write implementation resources under a private implementation directory. +write_resources(_Dir, []) -> + ok; +write_resources(Dir, [{Rel, Body} | Rest]) -> + Path = filename:join(Dir, hb_util:list(Rel)), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Body) of + ok -> + maybe_make_executable(Rel, Path), + write_resources(Dir, Rest); + {error, Reason} -> + {error, {resource_write_failed, Rel, Reason}} + end; + {error, Reason} -> + {error, {resource_dir_failed, Rel, Reason}} + end. + +%% @doc Mark scripts and bin resources executable after extraction. +maybe_make_executable(<<"bin/", _/binary>>, Path) -> + file:change_mode(Path, 8#100755); +maybe_make_executable(Rel, Path) -> + case filename:extension(hb_util:list(Rel)) of + ".sh" -> file:change_mode(Path, 8#100755); + _ -> ok + end. diff --git a/src/core/device/hb_device_load.erl b/src/core/device/hb_device_load.erl new file mode 100644 index 000000000..c2c5ce5b5 --- /dev/null +++ b/src/core/device/hb_device_load.erl @@ -0,0 +1,345 @@ +%%% @doc Resolve a device reference to its Erlang module. +%%% +%%% Two fundamental, mutually exclusive modes of device loading: +%%% +%%% Forge build. If `forge-bootstrap' maps a device name to a +%%% loaded module, that module is returned directly and never cached. +%%% Only the Forge build sets it, supplying the seed codecs under their +%%% module names so it can compute IDs and sign the preloaded messages. +%%% +%%% Runtime. Every device is loaded from an archive of compiled +%%% BEAM modules. Each module name is rewritten at compile time to the +%%% AO-Core ID of the source message that defined it, so every +%%% implementation has its own namespace and versions never conflict. +%%% Implementations are sourced most-trusted first: +%%% +%%%
    +%%%
  1. High trust (no signature check -- trusted as the +%%% runtime itself): the process cache; the operator's +%%% `trusted-devices' map; the build-signed preloaded store.
  2. +%%%
  3. Low trust (verified on first use): names resolved +%%% through the node's wider caches and Arweave, each +%%% implementation requiring a `trusted-device-signers' +%%% signature.
  4. +%%%
+-module(hb_device_load). +-export([reference/2]). +-include("include/hb.hrl"). + +%% @doc A message is already a device. A binary reference is resolved, +%% then memoised in the process cache unless it is a forge seed. +reference(Loaded, _Opts) when is_map(Loaded) -> + {ok, Loaded}; +reference(Ref, Opts) when is_binary(Ref) -> + NormRef = hb_ao:normalize_key(Ref), + case from_forge_bootstrap(NormRef, Opts) of + {ok, _} = Ok -> + Ok; + {error, not_found} -> + resolve_cached(NormRef, Opts); + {error, _} = Error -> + Error + end. + +resolve_cached(Ref, Opts) -> + case resolve(Ref, Opts) of + {ok, Mod} = Ok -> put_resolved_device(Ref, Mod, Opts), Ok; + {error, _} = Error -> Error + end. + +%% @doc The resolved-device store, then the high-trust sources, then the +%% low-trust sources. The first `{ok, _}' wins; a real error from a +%% trusted source is returned rather than falling through. +resolve(Ref, Opts) -> + maybe + {error, not_found} ?= get_resolved_device(Ref, Opts), + {error, not_found} ?= from_high_trust(Ref, Opts), + from_low_trust(Ref, Opts) + end. + +%% @doc Look up a previously-resolved module: the process dictionary +%% first, then the node's `loaded-device-store'. The first process to +%% resolve a device spares every other the index read and archive +%% extraction. The store defaults to `[]', which `hb_store' treats as +%% no viable store, so the shared tier disables itself with no branch. +get_resolved_device(Ref, Opts) -> + case erlang:get({?MODULE, Ref}) of + Mod when is_atom(Mod), Mod =/= undefined -> + {ok, Mod}; + _ -> + maybe + {ok, Bin} ?= + hb_store:read( + loaded_device_store(Opts), store_key(Ref), Opts), + Mod = hb_util:atom(Bin), + erlang:put({?MODULE, Ref}, Mod), + {ok, Mod} + end + end. + +%% @doc Memoise a resolved device in the process dictionary and the +%% shared `loaded-device-store'. +put_resolved_device(Ref, Mod, Opts) -> + erlang:put({?MODULE, Ref}, Mod), + hb_store:write( + loaded_device_store(Opts), + #{ store_key(Ref) => hb_util:bin(Mod) }, + Opts + ). + +loaded_device_store(Opts) -> hb_opts:get(loaded_device_store, [], Opts). + +store_key(Ref) -> <<"~meta@1.0/devices/", Ref/binary>>. + +%%% -------------------------------------------------------------------- +%%% High trust +%%% -------------------------------------------------------------------- + +from_high_trust(Ref, Opts) -> + maybe + {error, not_found} ?= from_trusted_devices(Ref, Opts), + from_preloaded(Ref, Opts) + end. + +%% @doc Forge-only map from seed device name to source module atom. +from_forge_bootstrap(Ref, Opts) -> + case hb_opts:get(forge_bootstrap, #{}, Opts) of + #{ Ref := Mod } when is_atom(Mod) -> {ok, Mod}; + Seeds when is_map(Seeds), map_size(Seeds) > 0 -> + {error, {forge_bootstrap_device_not_found, Ref}}; + _ -> {error, not_found} + end. + +%% @doc Operator-pinned map from device name or spec ID to implementation ID. +from_trusted_devices(Ref, Opts) -> + case hb_opts:get(trusted_devices, #{}, Opts) of + #{ Ref := ID } when ?IS_ID(ID) -> load_archive(ID, Opts); + _ -> {error, not_found} + end. + +%% @doc Resolve the device's spec ID from the flat preloaded index (or +%% use the reference directly if it is already an ID), then load the +%% first implementation that declares `implements-device' for it. The +%% read is codec-free: the codecs are themselves preloaded packages, +%% so decoding their messages cannot depend on a loaded codec. The +%% preloaded store is build-signed, so no signature check is needed. +from_preloaded(Ref, Opts) -> + case preloaded(Opts) of + undefined -> + {error, not_found}; + {Store, IndexID} -> + PreOpts = + Opts#{ <<"store">> => [Store], <<"cache-read-mode">> => raw }, + maybe + {ok, SpecID} ?= preloaded_spec(Ref, Store, IndexID, PreOpts), + lazy_first( + fun(ID) -> load_archive(ID, PreOpts) end, + [ + fun() -> + hb_util:ok_or( + hb_cache:match(implementation_query(SpecID), PreOpts), + [] + ) + end + ] + ) + end + end. + +preloaded_spec(Ref, _Store, _IndexID, _Opts) when ?IS_ID(Ref) -> + {ok, Ref}; +preloaded_spec(Ref, Store, IndexID, Opts) -> + hb_store:read(Store, <>, Opts). + +%% @doc The preloaded store and its signed index ID, from node config +%% (request-local cache keys stripped so it is visible inside a +%% request-scoped resolution). +preloaded(Opts) -> + Node = maps:without([<<"cache-control">>, <<"only">>, <<"prefer">>], Opts), + case + { + hb_opts:get(preloaded_store, undefined, Node), + hb_opts:get(preloaded_devices_index, undefined, Node) + } + of + {Store, IndexID} when Store =/= undefined, IndexID =/= undefined -> + {Store, IndexID}; + _ -> + undefined + end. + +%%% -------------------------------------------------------------------- +%%% Low trust +%%% -------------------------------------------------------------------- + +%% @doc Resolve the name through `name@1.0' (safe here -- the codecs are +%% already loaded via the high-trust path), then load the first signed, +%% compatible implementation. Local caches are always searched -- gateway +%% lookup is gated by `load-remote-devices'. +from_low_trust(Ref, Opts) -> + maybe + {ok, SpecID} ?= resolve_spec(Ref, Opts), + LocalIterators = + [ + fun() -> + hb_util:ok_or( + hb_cache:match(implementation_query(SpecID), Opts), + [] + ) + end + ], + RemoteIterators = + case hb_opts:get(<<"load-remote-devices">>, false, Opts) of + true -> + [ + fun() -> + hb_util:ok_or( + hb_client_gateway:device( + SpecID, + trusted_signers(Opts), + Opts + ), + [] + ) + end + ]; + false -> + [] + end, + lazy_first( + fun(ID) -> verify_and_load(SpecID, ID, Opts) end, + LocalIterators ++ RemoteIterators + ) + end. + +resolve_spec(Ref, _Opts) when ?IS_ID(Ref) -> + {ok, Ref}; +resolve_spec(Ref, Opts) -> + case + hb_ao:raw( + #{ <<"device">> => <<"name@1.0">> }, + #{ <<"path">> => Ref, <<"load">> => false }, + Opts + ) + of + {ok, SpecID} when ?IS_ID(SpecID) -> {ok, SpecID}; + _ -> {error, <<"device-name-not-resolvable">>} + end. + +%% @doc A low-trust implementation must be signed by a trusted signer, +%% implement the requested specification, and be machine-compatible. +verify_and_load(SpecID, ID, Opts) -> + maybe + {ok, Msg} ?= hb_cache:read(ID, Opts), + Signers = signers(Msg, Opts), + true ?= + hb_message:verify(Msg, Signers, Opts) + orelse {error, <<"implementation-signature-invalid">>}, + true ?= + lists:any( + fun(S) -> lists:member(S, trusted_signers(Opts)) end, + Signers + ) orelse {error, <<"device-signer-untrusted">>}, + ok ?= implements(SpecID, Msg, Opts), + ok ?= compatible(Msg, Opts), + load_archive_message(Msg, Opts) + end. + +%% @doc Apply `F' to each element produced by the iterators in turn, +%% returning the first `{ok, _}'. +lazy_first(F, Iterators) -> + lazy_first(F, [], Iterators). + +lazy_first(_F, [], []) -> + {error, not_found}; +lazy_first(F, [], [Next | Rest]) -> + lazy_first(F, Next(), Rest); +lazy_first(F, [X | Xs], Iterators) -> + case F(X) of + {ok, _} = Ok -> Ok; + {error, _} -> lazy_first(F, Xs, Iterators) + end. + +%%% -------------------------------------------------------------------- +%%% Archive loading, verification and shared helpers +%%% -------------------------------------------------------------------- + +%% @doc Load an implementation by its archive ID, trusting it (the +%% caller is a high-trust source). +load_archive(ID, Opts) -> + maybe + {ok, Msg} ?= hb_cache:read(ID, Opts), + load_archive_message(Msg, Opts) + end. + +load_archive_message(Msg, Opts) -> + hb_device_archive:load( + hb_maps:get(<<"module-name">>, Msg, undefined, Opts), + hb_maps:get(<<"body">>, Msg, undefined, Opts), + Msg, + Opts + ). + +implementation_query(SpecID) -> + #{ + <<"data-protocol">> => <<"ao">>, + <<"variant">> => <<"ao.N.1">>, + <<"content-type">> => <<"application/beam-archive">>, + <<"implements-device">> => SpecID + }. + +implements(SpecID, Msg, Opts) -> + case hb_maps:get(<<"implements-device">>, Msg, undefined, Opts) of + SpecID -> ok; + Other -> {error, {<<"wrong-device-specification">>, Other}} + end. + +%% @doc The commitment signers, read inline rather than via +%% `hb_message:signers/2' to avoid invoking `message@1.0'. +signers(Msg, Opts) -> + hb_maps:values( + hb_maps:filtermap( + fun(_ID, C) -> + case hb_maps:get(<<"committer">>, C, undefined, Opts) of + undefined -> false; + Signer -> {true, Signer} + end + end, + hb_maps:get(<<"commitments">>, Msg, #{}, Opts), + Opts + ), + Opts + ). +%% @doc Trusted signers, defaulting to the node's own address. +%% Computed lazily so the default config need not call `hb:address/0'. +trusted_signers(Opts) -> + case hb_opts:get(trusted_device_signers, [], Opts) of + [] -> [hb:address()]; + Signers when is_list(Signers) -> Signers + end. + +%% @doc Every `requires-*' key must match this machine's `system_info'. +compatible(Msg, Opts) -> + Failed = + lists:filtermap( + fun + ({<<"requires-", Key/binary>>, Value}) -> + Prop = + hb_util:key_to_atom( + hb_ao:normalize_key(Key), new_atoms), + Want = hb_cache:ensure_loaded(Value, Opts), + case + hb_ao:normalize_key(erlang:system_info(Prop)) + == hb_ao:normalize_key(Want) + of + true -> false; + false -> {true, {Prop, Want}} + end; + (_) -> + false + end, + hb_maps:to_list(Msg, Opts)), + case Failed of + [] -> ok; + _ -> {error, {failed_requirements, Failed}} + end. diff --git a/src/core/device/hb_device_name.erl b/src/core/device/hb_device_name.erl new file mode 100644 index 000000000..f393b4db5 --- /dev/null +++ b/src/core/device/hb_device_name.erl @@ -0,0 +1,62 @@ +%%% @doc Helpers for generated packaged-device module names. +-module(hb_device_name). +-export([generated/2, sanitize/1, is_generated/1, parts/1, root/1]). + +-define(PREFIX, <<"_hb_device_">>). + +%% @doc Build the generated module atom for a device and package hash. +generated(DeviceName, Hash) -> + binary_to_atom( + <>, + utf8 + ). + +%% @doc Sanitize a device name so it can appear inside an Erlang atom. +sanitize(Name) when is_binary(Name) -> + Lower = string:lowercase(Name), + list_to_binary([sanitize_char(C) || <> <= Lower]); +sanitize(Name) when is_list(Name) -> + sanitize(hb_util:bin(Name)); +sanitize(Name) when is_atom(Name) -> + sanitize(atom_to_binary(Name, utf8)). + +%% @doc Return a character safe for generated Erlang module atoms. +sanitize_char(C) when C >= $a, C =< $z -> C; +sanitize_char(C) when C >= $0, C =< $9 -> C; +sanitize_char(_) -> $_. + +%% @doc Recognize a generated `_hb_device_*' module atom or binary. +is_generated(Atom) when is_atom(Atom) -> + is_generated(atom_to_binary(Atom, utf8)); +is_generated(<<"_hb_device_", _/binary>>) -> + true; +is_generated(_) -> + false. + +%% @doc Decompose a generated module name into display components. +parts(Atom) when is_atom(Atom) -> + parts(atom_to_binary(Atom, utf8)); +parts(<<"_hb_device_", Rest/binary>>) -> + [RootPart | HelperParts] = binary:split(Rest, <<"__">>, [global]), + case binary:split(RootPart, <<"_">>, [global]) of + Parts when length(Parts) >= 2 -> + [Hash | RevName] = lists:reverse(Parts), + Name = + iolist_to_binary( + lists:join(<<"_">>, lists:reverse(RevName)) + ), + case HelperParts of + [] -> {Name, Hash}; + _ -> + Helper = iolist_to_binary(lists:join(<<"__">>, HelperParts)), + {Name, Hash, Helper} + end; + _ -> not_generated + end; +parts(_) -> + not_generated. + +%% @doc Return the root generated module for a root or helper module. +root(Module) -> + [Root | _] = binary:split(atom_to_binary(Module, utf8), <<"__">>), + binary_to_atom(Root, utf8). diff --git a/src/core/html/hyperbuddy@1.0/404.html b/src/core/html/hyperbuddy@1.0/404.html new file mode 100644 index 000000000..9786ba35a --- /dev/null +++ b/src/core/html/hyperbuddy@1.0/404.html @@ -0,0 +1,55 @@ + + + 404 - Page not found. + + + + + + + + + +
+
+

404.

+

Page cannot be found.

+

+ This hashpath cannot be resolved on this node, yet + ... + +

+
+
+ + + diff --git a/src/core/html/hyperbuddy@1.0/500.html b/src/core/html/hyperbuddy@1.0/500.html new file mode 100644 index 000000000..a6fa2de86 --- /dev/null +++ b/src/core/html/hyperbuddy@1.0/500.html @@ -0,0 +1,45 @@ + + + 500 - Oops. + + + + + + + + + +
+
+

500.

+

Oops, your hashpath couldn't be resolved right now.

+

{{error}}

+
+
+ + + diff --git a/src/core/html/hyperbuddy@1.0/bundle.js b/src/core/html/hyperbuddy@1.0/bundle.js new file mode 100644 index 000000000..790ebea66 --- /dev/null +++ b/src/core/html/hyperbuddy@1.0/bundle.js @@ -0,0 +1,9682 @@ +var d8=Object.defineProperty;var p8=(J0,Xh,LB)=>Xh in J0?d8(J0,Xh,{enumerable:!0,configurable:!0,writable:!0,value:LB}):J0[Xh]=LB;var cf=(J0,Xh,LB)=>p8(J0,typeof Xh!="symbol"?Xh+"":Xh,LB);(function(){"use strict";var J0,Xh;function _mergeNamespaces(eA,AA){for(var tA=0;tArA[iA]})}}}return Object.freeze(Object.defineProperty(eA,Symbol.toStringTag,{value:"Module"}))}function getDefaultExportFromCjs$1(eA){return eA&&eA.__esModule&&Object.prototype.hasOwnProperty.call(eA,"default")?eA.default:eA}var browser$d={exports:{}},process=browser$d.exports={},cachedSetTimeout$1,cachedClearTimeout$1;function defaultSetTimout$1(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout$1(){throw new Error("clearTimeout has not been defined")}(function(){try{typeof setTimeout=="function"?cachedSetTimeout$1=setTimeout:cachedSetTimeout$1=defaultSetTimout$1}catch{cachedSetTimeout$1=defaultSetTimout$1}try{typeof clearTimeout=="function"?cachedClearTimeout$1=clearTimeout:cachedClearTimeout$1=defaultClearTimeout$1}catch{cachedClearTimeout$1=defaultClearTimeout$1}})();function runTimeout$1(eA){if(cachedSetTimeout$1===setTimeout)return setTimeout(eA,0);if((cachedSetTimeout$1===defaultSetTimout$1||!cachedSetTimeout$1)&&setTimeout)return cachedSetTimeout$1=setTimeout,setTimeout(eA,0);try{return cachedSetTimeout$1(eA,0)}catch{try{return cachedSetTimeout$1.call(null,eA,0)}catch{return cachedSetTimeout$1.call(this,eA,0)}}}function runClearTimeout$1(eA){if(cachedClearTimeout$1===clearTimeout)return clearTimeout(eA);if((cachedClearTimeout$1===defaultClearTimeout$1||!cachedClearTimeout$1)&&clearTimeout)return cachedClearTimeout$1=clearTimeout,clearTimeout(eA);try{return cachedClearTimeout$1(eA)}catch{try{return cachedClearTimeout$1.call(null,eA)}catch{return cachedClearTimeout$1.call(this,eA)}}}var queue$1=[],draining$1=!1,currentQueue$1,queueIndex$1=-1;function cleanUpNextTick$1(){!draining$1||!currentQueue$1||(draining$1=!1,currentQueue$1.length?queue$1=currentQueue$1.concat(queue$1):queueIndex$1=-1,queue$1.length&&drainQueue$1())}function drainQueue$1(){if(!draining$1){var eA=runTimeout$1(cleanUpNextTick$1);draining$1=!0;for(var AA=queue$1.length;AA;){for(currentQueue$1=queue$1,queue$1=[];++queueIndex$11)for(var tA=1;tA1)for(var tA=1;tA1?qa-1:0),_s=1;_s1?qa-1:0),_s=1;_s1){for(var Tl=Array(ul),Zu=0;Zu1){for(var Vu=Array(Zu),el=0;el is not supported and will be removed in a future major release. Did you mean to render instead?")),qa.Provider},set:function(eu){qa.Provider=eu}},_currentValue:{get:function(){return qa._currentValue},set:function(eu){qa._currentValue=eu}},_currentValue2:{get:function(){return qa._currentValue2},set:function(eu){qa._currentValue2=eu}},_threadCount:{get:function(){return qa._threadCount},set:function(eu){qa._threadCount=eu}},Consumer:{get:function(){return Ms||(Ms=!0,_A("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),qa.Consumer}},displayName:{get:function(){return qa.displayName},set:function(eu){js||(MA("Setting `displayName` on Context.Consumer has no effect. You should set it directly on the context with Context.displayName = '%s'.",eu),js=!0)}}}),qa.Consumer=Du}return qa._currentRenderer=null,qa._currentRenderer2=null,qa}var fa=-1,ma=0,Fa=1,os=2;function Oa(va){if(va._status===fa){var qa=va._result,Ms=qa();if(Ms.then(function(Du){if(va._status===ma||va._status===fa){var eu=va;eu._status=Fa,eu._result=Du}},function(Du){if(va._status===ma||va._status===fa){var eu=va;eu._status=os,eu._result=Du}}),va._status===fa){var _s=va;_s._status=ma,_s._result=Ms}}if(va._status===Fa){var js=va._result;return js===void 0&&_A(`lazy: Expected the result of a dynamic import() call. Instead received: %s + +Your code should look like: + const MyComponent = lazy(() => import('./MyComponent')) + +Did you accidentally put curly braces around the import?`,js),"default"in js||_A(`lazy: Expected the result of a dynamic import() call. Instead received: %s + +Your code should look like: + const MyComponent = lazy(() => import('./MyComponent'))`,js),js.default}else throw va._result}function hs(va){var qa={_status:fa,_result:va},Ms={$$typeof:EA,_payload:qa,_init:Oa};{var _s,js;Object.defineProperties(Ms,{defaultProps:{configurable:!0,get:function(){return _s},set:function(Du){_A("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),_s=Du,Object.defineProperty(Ms,"defaultProps",{enumerable:!0})}},propTypes:{configurable:!0,get:function(){return js},set:function(Du){_A("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),js=Du,Object.defineProperty(Ms,"propTypes",{enumerable:!0})}}})}return Ms}function ru(va){va!=null&&va.$$typeof===vA?_A("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof va!="function"?_A("forwardRef requires a render function but was given %s.",va===null?"null":typeof va):va.length!==0&&va.length!==2&&_A("forwardRef render functions accept exactly two parameters: props and ref. %s",va.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),va!=null&&(va.defaultProps!=null||va.propTypes!=null)&&_A("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?");var qa={$$typeof:cA,render:va};{var Ms;Object.defineProperty(qa,"displayName",{enumerable:!1,configurable:!0,get:function(){return Ms},set:function(_s){Ms=_s,!va.name&&!va.displayName&&(va.displayName=_s)}})}return qa}var Ca;Ca=Symbol.for("react.module.reference");function $a(va){return!!(typeof va=="string"||typeof va=="function"||va===nA||va===aA||BA||va===oA||va===dA||va===mA||gA||va===CA||WA||PA||sA||typeof va=="object"&&va!==null&&(va.$$typeof===EA||va.$$typeof===vA||va.$$typeof===uA||va.$$typeof===lA||va.$$typeof===cA||va.$$typeof===Ca||va.getModuleId!==void 0))}function xs(va,qa){$a(va)||_A("memo: The first argument must be a component. Instead received: %s",va===null?"null":typeof va);var Ms={$$typeof:vA,type:va,compare:qa===void 0?null:qa};{var _s;Object.defineProperty(Ms,"displayName",{enumerable:!1,configurable:!0,get:function(){return _s},set:function(js){_s=js,!va.name&&!va.displayName&&(va.displayName=js)}})}return Ms}function es(){var va=GA.current;return va===null&&_A(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: +1. You might have mismatching versions of React and the renderer (such as React DOM) +2. You might be breaking the Rules of Hooks +3. You might have more than one copy of React in the same app +See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.`),va}function ps(va){var qa=es();if(va._context!==void 0){var Ms=va._context;Ms.Consumer===va?_A("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"):Ms.Provider===va&&_A("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?")}return qa.useContext(va)}function Js(va){var qa=es();return qa.useState(va)}function Xs(va,qa,Ms){var _s=es();return _s.useReducer(va,qa,Ms)}function lu(va){var qa=es();return qa.useRef(va)}function Fu(va,qa){var Ms=es();return Ms.useEffect(va,qa)}function Mu(va,qa){var Ms=es();return Ms.useInsertionEffect(va,qa)}function xa(va,qa){var Ms=es();return Ms.useLayoutEffect(va,qa)}function Za(va,qa){var Ms=es();return Ms.useCallback(va,qa)}function fs(va,qa){var Ms=es();return Ms.useMemo(va,qa)}function ds(va,qa,Ms){var _s=es();return _s.useImperativeHandle(va,qa,Ms)}function Ja(va,qa){{var Ms=es();return Ms.useDebugValue(va,qa)}}function Es(){var va=es();return va.useTransition()}function Ls(va){var qa=es();return qa.useDeferredValue(va)}function Ws(){var va=es();return va.useId()}function Os(va,qa,Ms){var _s=es();return _s.useSyncExternalStore(va,qa,Ms)}var qs=0,Zs,Vs,Au,vu,Ju,al,Bl;function wu(){}wu.__reactDisabledLog=!0;function gl(){{if(qs===0){Zs=console.log,Vs=console.info,Au=console.warn,vu=console.error,Ju=console.group,al=console.groupCollapsed,Bl=console.groupEnd;var va={configurable:!0,enumerable:!0,value:wu,writable:!0};Object.defineProperties(console,{info:va,log:va,warn:va,error:va,group:va,groupCollapsed:va,groupEnd:va})}qs++}}function ku(){{if(qs--,qs===0){var va={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:kA({},va,{value:Zs}),info:kA({},va,{value:Vs}),warn:kA({},va,{value:Au}),error:kA({},va,{value:vu}),group:kA({},va,{value:Ju}),groupCollapsed:kA({},va,{value:al}),groupEnd:kA({},va,{value:Bl})})}qs<0&&_A("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var Ul=wA.ReactCurrentDispatcher,uc;function lc(va,qa,Ms){{if(uc===void 0)try{throw Error()}catch(js){var _s=js.stack.trim().match(/\n( *(at )?)/);uc=_s&&_s[1]||""}return` +`+uc+va}}var zf=!1,ml;{var mc=typeof WeakMap=="function"?WeakMap:Map;ml=new mc}function cc(va,qa){if(!va||zf)return"";{var Ms=ml.get(va);if(Ms!==void 0)return Ms}var _s;zf=!0;var js=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var Du;Du=Ul.current,Ul.current=null,gl();try{if(qa){var eu=function(){throw Error()};if(Object.defineProperty(eu.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(eu,[])}catch(Xl){_s=Xl}Reflect.construct(va,[],eu)}else{try{eu.call()}catch(Xl){_s=Xl}va.call(eu.prototype)}}else{try{throw Error()}catch(Xl){_s=Xl}va()}}catch(Xl){if(Xl&&_s&&typeof Xl.stack=="string"){for(var xu=Xl.stack.split(` +`),Xu=_s.stack.split(` +`),ul=xu.length-1,Tl=Xu.length-1;ul>=1&&Tl>=0&&xu[ul]!==Xu[Tl];)Tl--;for(;ul>=1&&Tl>=0;ul--,Tl--)if(xu[ul]!==Xu[Tl]){if(ul!==1||Tl!==1)do if(ul--,Tl--,Tl<0||xu[ul]!==Xu[Tl]){var Zu=` +`+xu[ul].replace(" at new "," at ");return va.displayName&&Zu.includes("")&&(Zu=Zu.replace("",va.displayName)),typeof va=="function"&&ml.set(va,Zu),Zu}while(ul>=1&&Tl>=0);break}}}finally{zf=!1,Ul.current=Du,ku(),Error.prepareStackTrace=js}var Vu=va?va.displayName||va.name:"",el=Vu?lc(Vu):"";return typeof va=="function"&&ml.set(va,el),el}function zl(va,qa,Ms){return cc(va,!1)}function Dc(va){var qa=va.prototype;return!!(qa&&qa.isReactComponent)}function ff(va,qa,Ms){if(va==null)return"";if(typeof va=="function")return cc(va,Dc(va));if(typeof va=="string")return lc(va);switch(va){case dA:return lc("Suspense");case mA:return lc("SuspenseList")}if(typeof va=="object")switch(va.$$typeof){case cA:return zl(va.render);case vA:return ff(va.type,qa,Ms);case EA:{var _s=va,js=_s._payload,Du=_s._init;try{return ff(Du(js),qa,Ms)}catch{}}}return""}var Ku={},Fh=wA.ReactDebugCurrentFrame;function sl(va){if(va){var qa=va._owner,Ms=ff(va.type,va._source,qa?qa.type:null);Fh.setExtraStackFrame(Ms)}else Fh.setExtraStackFrame(null)}function Sp(va,qa,Ms,_s,js){{var Du=Function.call.bind(Na);for(var eu in va)if(Du(va,eu)){var xu=void 0;try{if(typeof va[eu]!="function"){var Xu=Error((_s||"React class")+": "+Ms+" type `"+eu+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof va[eu]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw Xu.name="Invariant Violation",Xu}xu=va[eu](qa,eu,_s,Ms,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(ul){xu=ul}xu&&!(xu instanceof Error)&&(sl(js),_A("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",_s||"React class",Ms,eu,typeof xu),sl(null)),xu instanceof Error&&!(xu.message in Ku)&&(Ku[xu.message]=!0,sl(js),_A("Failed %s type: %s",Ms,xu.message),sl(null))}}}function Zh(va){if(va){var qa=va._owner,Ms=ff(va.type,va._source,qa?qa.type:null);jA(Ms)}else jA(null)}var Lu;Lu=!1;function Hd(){if(OA.current){var va=is(OA.current.type);if(va)return` + +Check the render method of \``+va+"`."}return""}function Gc(va){if(va!==void 0){var qa=va.fileName.replace(/^.*[\\\/]/,""),Ms=va.lineNumber;return` + +Check your code at `+qa+":"+Ms+"."}return""}function Yh(va){return va!=null?Gc(va.__source):""}var hf={};function Ag(va){var qa=Hd();if(!qa){var Ms=typeof va=="string"?va:va.displayName||va.name;Ms&&(qa=` + +Check the top-level render call using <`+Ms+">.")}return qa}function jl(va,qa){if(!(!va._store||va._store.validated||va.key!=null)){va._store.validated=!0;var Ms=Ag(qa);if(!hf[Ms]){hf[Ms]=!0;var _s="";va&&va._owner&&va._owner!==OA.current&&(_s=" It was passed a child from "+is(va._owner.type)+"."),Zh(va),_A('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',Ms,_s),Zh(null)}}}function _l(va,qa){if(typeof va=="object"){if(na(va))for(var Ms=0;Ms",js=" Did you accidentally export a JSX literal instead of a component?"):eu=typeof va,_A("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",eu,js)}var xu=Xi.apply(this,arguments);if(xu==null)return xu;if(_s)for(var Xu=2;Xu10&&MA("Detected a large number of updates inside startTransition. If this is due to a subscription please re-write it to use React provided hooks. Otherwise concurrent mode guarantees are off the table."),_s._updatedFibers.clear()}}}var ed=!1,kd=null;function dv(va){if(kd===null)try{var qa=("require"+Math.random()).slice(0,7),Ms=eA&&eA[qa];kd=Ms.call(eA,"timers").setImmediate}catch{kd=function(js){ed===!1&&(ed=!0,typeof MessageChannel>"u"&&_A("This browser does not have a MessageChannel implementation, so enqueuing tasks via await act(async () => ...) will fail. Please file an issue at https://github.com/facebook/react/issues if you encounter this warning."));var Du=new MessageChannel;Du.port1.onmessage=js,Du.port2.postMessage(void 0)}}return kd(va)}var nl=0,il=!1;function eg(va){{var qa=nl;nl++,UA.current===null&&(UA.current=[]);var Ms=UA.isBatchingLegacy,_s;try{if(UA.isBatchingLegacy=!0,_s=va(),!Ms&&UA.didScheduleLegacyUpdate){var js=UA.current;js!==null&&(UA.didScheduleLegacyUpdate=!1,td(js))}}catch(Vu){throw uh(qa),Vu}finally{UA.isBatchingLegacy=Ms}if(_s!==null&&typeof _s=="object"&&typeof _s.then=="function"){var Du=_s,eu=!1,xu={then:function(Vu,el){eu=!0,Du.then(function(Xl){uh(qa),nl===0?_p(Xl,Vu,el):Vu(Xl)},function(Xl){uh(qa),el(Xl)})}};return!il&&typeof Promise<"u"&&Promise.resolve().then(function(){}).then(function(){eu||(il=!0,_A("You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);"))}),xu}else{var Xu=_s;if(uh(qa),nl===0){var ul=UA.current;ul!==null&&(td(ul),UA.current=null);var Tl={then:function(Vu,el){UA.current===null?(UA.current=[],_p(Xu,Vu,el)):Vu(Xu)}};return Tl}else{var Zu={then:function(Vu,el){Vu(Xu)}};return Zu}}}}function uh(va){va!==nl-1&&_A("You seem to have overlapping act() calls, this is not supported. Be sure to await previous act() calls before making a new one. "),nl=va}function _p(va,qa,Ms){{var _s=UA.current;if(_s!==null)try{td(_s),dv(function(){_s.length===0?(UA.current=null,qa(va)):_p(va,qa,Ms)})}catch(js){Ms(js)}else qa(va)}}var Fp=!1;function td(va){if(!Fp){Fp=!0;var qa=0;try{for(;qa1?$a-1:0),es=1;es<$a;es++)xs[es-1]=arguments[es];GA("error",Ca,xs)}}function GA(Ca,$a,xs){{var es=QA.ReactDebugCurrentFrame,ps=es.getStackAddendum();ps!==""&&($a+="%s",xs=xs.concat([ps]));var Js=xs.map(function(Xs){return String(Xs)});Js.unshift("Warning: "+$a),Function.prototype.apply.call(console[Ca],console,Js)}}var YA=!1,UA=!1,OA=!1,VA=!1,Qi=!1,jA;jA=Symbol.for("react.module.reference");function WA(Ca){return!!(typeof Ca=="string"||typeof Ca=="function"||Ca===rA||Ca===nA||Qi||Ca===iA||Ca===lA||Ca===cA||VA||Ca===vA||YA||UA||OA||typeof Ca=="object"&&Ca!==null&&(Ca.$$typeof===mA||Ca.$$typeof===dA||Ca.$$typeof===oA||Ca.$$typeof===aA||Ca.$$typeof===uA||Ca.$$typeof===jA||Ca.getModuleId!==void 0))}function PA(Ca,$a,xs){var es=Ca.displayName;if(es)return es;var ps=$a.displayName||$a.name||"";return ps!==""?xs+"("+ps+")":xs}function sA(Ca){return Ca.displayName||"Context"}function gA(Ca){if(Ca==null)return null;if(typeof Ca.tag=="number"&&FA("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),typeof Ca=="function")return Ca.displayName||Ca.name||null;if(typeof Ca=="string")return Ca;switch(Ca){case rA:return"Fragment";case tA:return"Portal";case nA:return"Profiler";case iA:return"StrictMode";case lA:return"Suspense";case cA:return"SuspenseList"}if(typeof Ca=="object")switch(Ca.$$typeof){case aA:var $a=Ca;return sA($a)+".Consumer";case oA:var xs=Ca;return sA(xs._context)+".Provider";case uA:return PA(Ca,Ca.render,"ForwardRef");case dA:var es=Ca.displayName||null;return es!==null?es:gA(Ca.type)||"Memo";case mA:{var ps=Ca,Js=ps._payload,Xs=ps._init;try{return gA(Xs(Js))}catch{return null}}}return null}var BA=Object.assign,wA=0,MA,_A,TA,bA,hA,DA,kA;function $A(){}$A.__reactDisabledLog=!0;function qi(){{if(wA===0){MA=console.log,_A=console.info,TA=console.warn,bA=console.error,hA=console.group,DA=console.groupCollapsed,kA=console.groupEnd;var Ca={configurable:!0,enumerable:!0,value:$A,writable:!0};Object.defineProperties(console,{info:Ca,log:Ca,warn:Ca,error:Ca,group:Ca,groupCollapsed:Ca,groupEnd:Ca})}wA++}}function aa(){{if(wA--,wA===0){var Ca={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:BA({},Ca,{value:MA}),info:BA({},Ca,{value:_A}),warn:BA({},Ca,{value:TA}),error:BA({},Ca,{value:bA}),group:BA({},Ca,{value:hA}),groupCollapsed:BA({},Ca,{value:DA}),groupEnd:BA({},Ca,{value:kA})})}wA<0&&FA("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var Fi=QA.ReactCurrentDispatcher,bt;function Yi(Ca,$a,xs){{if(bt===void 0)try{throw Error()}catch(ps){var es=ps.stack.trim().match(/\n( *(at )?)/);bt=es&&es[1]||""}return` +`+bt+Ca}}var ta=!1,ca;{var oa=typeof WeakMap=="function"?WeakMap:Map;ca=new oa}function $i(Ca,$a){if(!Ca||ta)return"";{var xs=ca.get(Ca);if(xs!==void 0)return xs}var es;ta=!0;var ps=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var Js;Js=Fi.current,Fi.current=null,qi();try{if($a){var Xs=function(){throw Error()};if(Object.defineProperty(Xs.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(Xs,[])}catch(Ja){es=Ja}Reflect.construct(Ca,[],Xs)}else{try{Xs.call()}catch(Ja){es=Ja}Ca.call(Xs.prototype)}}else{try{throw Error()}catch(Ja){es=Ja}Ca()}}catch(Ja){if(Ja&&es&&typeof Ja.stack=="string"){for(var lu=Ja.stack.split(` +`),Fu=es.stack.split(` +`),Mu=lu.length-1,xa=Fu.length-1;Mu>=1&&xa>=0&&lu[Mu]!==Fu[xa];)xa--;for(;Mu>=1&&xa>=0;Mu--,xa--)if(lu[Mu]!==Fu[xa]){if(Mu!==1||xa!==1)do if(Mu--,xa--,xa<0||lu[Mu]!==Fu[xa]){var Za=` +`+lu[Mu].replace(" at new "," at ");return Ca.displayName&&Za.includes("")&&(Za=Za.replace("",Ca.displayName)),typeof Ca=="function"&&ca.set(Ca,Za),Za}while(Mu>=1&&xa>=0);break}}}finally{ta=!1,Fi.current=Js,aa(),Error.prepareStackTrace=ps}var fs=Ca?Ca.displayName||Ca.name:"",ds=fs?Yi(fs):"";return typeof Ca=="function"&&ca.set(Ca,ds),ds}function na(Ca,$a,xs){return $i(Ca,!1)}function Sa(Ca){var $a=Ca.prototype;return!!($a&&$a.isReactComponent)}function wa(Ca,$a,xs){if(Ca==null)return"";if(typeof Ca=="function")return $i(Ca,Sa(Ca));if(typeof Ca=="string")return Yi(Ca);switch(Ca){case lA:return Yi("Suspense");case cA:return Yi("SuspenseList")}if(typeof Ca=="object")switch(Ca.$$typeof){case uA:return na(Ca.render);case dA:return wa(Ca.type,$a,xs);case mA:{var es=Ca,ps=es._payload,Js=es._init;try{return wa(Js(ps),$a,xs)}catch{}}}return""}var Ia=Object.prototype.hasOwnProperty,Ra={},Ta=QA.ReactDebugCurrentFrame;function Ma(Ca){if(Ca){var $a=Ca._owner,xs=wa(Ca.type,Ca._source,$a?$a.type:null);Ta.setExtraStackFrame(xs)}else Ta.setExtraStackFrame(null)}function is(Ca,$a,xs,es,ps){{var Js=Function.call.bind(Ia);for(var Xs in Ca)if(Js(Ca,Xs)){var lu=void 0;try{if(typeof Ca[Xs]!="function"){var Fu=Error((es||"React class")+": "+xs+" type `"+Xs+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof Ca[Xs]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw Fu.name="Invariant Violation",Fu}lu=Ca[Xs]($a,Xs,es,xs,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(Mu){lu=Mu}lu&&!(lu instanceof Error)&&(Ma(ps),FA("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",es||"React class",xs,Xs,typeof lu),Ma(null)),lu instanceof Error&&!(lu.message in Ra)&&(Ra[lu.message]=!0,Ma(ps),FA("Failed %s type: %s",xs,lu.message),Ma(null))}}}var Na=Array.isArray;function ja(Ca){return Na(Ca)}function ya(Ca){{var $a=typeof Symbol=="function"&&Symbol.toStringTag,xs=$a&&Ca[Symbol.toStringTag]||Ca.constructor.name||"Object";return xs}}function sa(Ca){try{return ba(Ca),!1}catch{return!0}}function ba(Ca){return""+Ca}function ts(Ca){if(sa(Ca))return FA("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",ya(Ca)),ba(Ca)}var qA=QA.ReactCurrentOwner,Zi={key:!0,ref:!0,__self:!0,__source:!0},da,Bi;function KA(Ca){if(Ia.call(Ca,"ref")){var $a=Object.getOwnPropertyDescriptor(Ca,"ref").get;if($a&&$a.isReactWarning)return!1}return Ca.ref!==void 0}function Xi(Ca){if(Ia.call(Ca,"key")){var $a=Object.getOwnPropertyDescriptor(Ca,"key").get;if($a&&$a.isReactWarning)return!1}return Ca.key!==void 0}function ia(Ca,$a){typeof Ca.ref=="string"&&qA.current}function pa(Ca,$a){{var xs=function(){da||(da=!0,FA("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",$a))};xs.isReactWarning=!0,Object.defineProperty(Ca,"key",{get:xs,configurable:!0})}}function Ha(Ca,$a){{var xs=function(){Bi||(Bi=!0,FA("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",$a))};xs.isReactWarning=!0,Object.defineProperty(Ca,"ref",{get:xs,configurable:!0})}}var Pa=function(Ca,$a,xs,es,ps,Js,Xs){var lu={$$typeof:AA,type:Ca,key:$a,ref:xs,props:Xs,_owner:Js};return lu._store={},Object.defineProperty(lu._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(lu,"_self",{configurable:!1,enumerable:!1,writable:!1,value:es}),Object.defineProperty(lu,"_source",{configurable:!1,enumerable:!1,writable:!1,value:ps}),Object.freeze&&(Object.freeze(lu.props),Object.freeze(lu)),lu};function La(Ca,$a,xs,es,ps){{var Js,Xs={},lu=null,Fu=null;xs!==void 0&&(ts(xs),lu=""+xs),Xi($a)&&(ts($a.key),lu=""+$a.key),KA($a)&&(Fu=$a.ref,ia($a,ps));for(Js in $a)Ia.call($a,Js)&&!Zi.hasOwnProperty(Js)&&(Xs[Js]=$a[Js]);if(Ca&&Ca.defaultProps){var Mu=Ca.defaultProps;for(Js in Mu)Xs[Js]===void 0&&(Xs[Js]=Mu[Js])}if(lu||Fu){var xa=typeof Ca=="function"?Ca.displayName||Ca.name||"Unknown":Ca;lu&&pa(Xs,xa),Fu&&Ha(Xs,xa)}return Pa(Ca,lu,Fu,ps,es,qA.current,Xs)}}var Qs=QA.ReactCurrentOwner,Qa=QA.ReactDebugCurrentFrame;function za(Ca){if(Ca){var $a=Ca._owner,xs=wa(Ca.type,Ca._source,$a?$a.type:null);Qa.setExtraStackFrame(xs)}else Qa.setExtraStackFrame(null)}var Bs;Bs=!1;function Xa(Ca){return typeof Ca=="object"&&Ca!==null&&Ca.$$typeof===AA}function Ka(){{if(Qs.current){var Ca=gA(Qs.current.type);if(Ca)return` + +Check the render method of \``+Ca+"`."}return""}}function vs(Ca){return""}var As={};function ls(Ca){{var $a=Ka();if(!$a){var xs=typeof Ca=="string"?Ca:Ca.displayName||Ca.name;xs&&($a=` + +Check the top-level render call using <`+xs+">.")}return $a}}function tu(Ca,$a){{if(!Ca._store||Ca._store.validated||Ca.key!=null)return;Ca._store.validated=!0;var xs=ls($a);if(As[xs])return;As[xs]=!0;var es="";Ca&&Ca._owner&&Ca._owner!==Qs.current&&(es=" It was passed a child from "+gA(Ca._owner.type)+"."),za(Ca),FA('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',xs,es),za(null)}}function ha(Ca,$a){{if(typeof Ca!="object")return;if(ja(Ca))for(var xs=0;xs",lu=" Did you accidentally export a JSX literal instead of a component?"):Mu=typeof Ca,FA("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",Mu,lu)}var xa=La(Ca,$a,xs,ps,Js);if(xa==null)return xa;if(Xs){var Za=$a.children;if(Za!==void 0)if(es)if(ja(Za)){for(var fs=0;fs0?"{key: someKey, "+Ja.join(": ..., ")+": ...}":"{key: someKey}";if(!ma[ds+Es]){var Ls=Ja.length>0?"{"+Ja.join(": ..., ")+": ...}":"{}";FA(`A props object containing a "key" prop is being spread into JSX: + let props = %s; + <%s {...props} /> +React keys must be passed directly to JSX without using spread: + let props = %s; + <%s key={someKey} {...props} />`,Es,ds,Ls,ds),ma[ds+Es]=!0}}return Ca===rA?fa(xa):ea(xa),xa}}function os(Ca,$a,xs){return Fa(Ca,$a,xs,!0)}function Oa(Ca,$a,xs){return Fa(Ca,$a,xs,!1)}var hs=Oa,ru=os;reactJsxRuntime_development.Fragment=rA,reactJsxRuntime_development.jsx=hs,reactJsxRuntime_development.jsxs=ru})()),reactJsxRuntime_development}var hasRequiredJsxRuntime;function requireJsxRuntime(){return hasRequiredJsxRuntime||(hasRequiredJsxRuntime=1,browser$1$1.env.NODE_ENV==="production"?jsxRuntime.exports=requireReactJsxRuntime_production_min():jsxRuntime.exports=requireReactJsxRuntime_development()),jsxRuntime.exports}var jsxRuntimeExports=requireJsxRuntime(),client={},reactDom={exports:{}},reactDom_production_min={},scheduler={exports:{}},scheduler_production_min={};/** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var hasRequiredScheduler_production_min;function requireScheduler_production_min(){return hasRequiredScheduler_production_min||(hasRequiredScheduler_production_min=1,(function(eA){function AA(TA,bA){var hA=TA.length;TA.push(bA);A:for(;0>>1,kA=TA[DA];if(0>>1;DA<$A;){var qi=2*(DA+1)-1,aa=TA[qi],Fi=qi+1,bt=TA[Fi];if(0>iA(aa,hA))FiiA(bt,aa)?(TA[DA]=bt,TA[Fi]=hA,DA=Fi):(TA[DA]=aa,TA[qi]=hA,DA=qi);else if(FiiA(bt,hA))TA[DA]=bt,TA[Fi]=hA,DA=Fi;else break A}}return bA}function iA(TA,bA){var hA=TA.sortIndex-bA.sortIndex;return hA!==0?hA:TA.id-bA.id}if(typeof performance=="object"&&typeof performance.now=="function"){var nA=performance;eA.unstable_now=function(){return nA.now()}}else{var oA=Date,aA=oA.now();eA.unstable_now=function(){return oA.now()-aA}}var uA=[],lA=[],cA=1,dA=null,mA=3,vA=!1,EA=!1,CA=!1,IA=typeof setTimeout=="function"?setTimeout:null,QA=typeof clearTimeout=="function"?clearTimeout:null,FA=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function GA(TA){for(var bA=tA(lA);bA!==null;){if(bA.callback===null)rA(lA);else if(bA.startTime<=TA)rA(lA),bA.sortIndex=bA.expirationTime,AA(uA,bA);else break;bA=tA(lA)}}function YA(TA){if(CA=!1,GA(TA),!EA)if(tA(uA)!==null)EA=!0,MA(UA);else{var bA=tA(lA);bA!==null&&_A(YA,bA.startTime-TA)}}function UA(TA,bA){EA=!1,CA&&(CA=!1,QA(Qi),Qi=-1),vA=!0;var hA=mA;try{for(GA(bA),dA=tA(uA);dA!==null&&(!(dA.expirationTime>bA)||TA&&!PA());){var DA=dA.callback;if(typeof DA=="function"){dA.callback=null,mA=dA.priorityLevel;var kA=DA(dA.expirationTime<=bA);bA=eA.unstable_now(),typeof kA=="function"?dA.callback=kA:dA===tA(uA)&&rA(uA),GA(bA)}else rA(uA);dA=tA(uA)}if(dA!==null)var $A=!0;else{var qi=tA(lA);qi!==null&&_A(YA,qi.startTime-bA),$A=!1}return $A}finally{dA=null,mA=hA,vA=!1}}var OA=!1,VA=null,Qi=-1,jA=5,WA=-1;function PA(){return!(eA.unstable_now()-WATA||125DA?(TA.sortIndex=hA,AA(lA,TA),tA(uA)===null&&TA===tA(lA)&&(CA?(QA(Qi),Qi=-1):CA=!0,_A(YA,hA-DA))):(TA.sortIndex=kA,AA(uA,TA),EA||vA||(EA=!0,MA(UA))),TA},eA.unstable_shouldYield=PA,eA.unstable_wrapCallback=function(TA){var bA=mA;return function(){var hA=mA;mA=bA;try{return TA.apply(this,arguments)}finally{mA=hA}}}})(scheduler_production_min)),scheduler_production_min}var scheduler_development={},hasRequiredScheduler_development;function requireScheduler_development(){return hasRequiredScheduler_development||(hasRequiredScheduler_development=1,(function(eA){browser$1$1.env.NODE_ENV!=="production"&&(function(){typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error);var AA=!1,tA=5;function rA(Bi,KA){var Xi=Bi.length;Bi.push(KA),oA(Bi,KA,Xi)}function iA(Bi){return Bi.length===0?null:Bi[0]}function nA(Bi){if(Bi.length===0)return null;var KA=Bi[0],Xi=Bi.pop();return Xi!==KA&&(Bi[0]=Xi,aA(Bi,Xi,0)),KA}function oA(Bi,KA,Xi){for(var ia=Xi;ia>0;){var pa=ia-1>>>1,Ha=Bi[pa];if(uA(Ha,KA)>0)Bi[pa]=KA,Bi[ia]=Ha,ia=pa;else return}}function aA(Bi,KA,Xi){for(var ia=Xi,pa=Bi.length,Ha=pa>>>1;iaXi&&(!Bi||Ta()));){var ia=sA.callback;if(typeof ia=="function"){sA.callback=null,gA=sA.priorityLevel;var pa=sA.expirationTime<=Xi,Ha=ia(pa);Xi=eA.unstable_now(),typeof Ha=="function"?sA.callback=Ha:sA===iA(jA)&&nA(jA),hA(Xi)}else nA(jA);sA=iA(jA)}if(sA!==null)return!0;var Pa=iA(WA);return Pa!==null&&ts(DA,Pa.startTime-Xi),!1}function qi(Bi,KA){switch(Bi){case lA:case cA:case dA:case mA:case vA:break;default:Bi=dA}var Xi=gA;gA=Bi;try{return KA()}finally{gA=Xi}}function aa(Bi){var KA;switch(gA){case lA:case cA:case dA:KA=dA;break;default:KA=gA;break}var Xi=gA;gA=KA;try{return Bi()}finally{gA=Xi}}function Fi(Bi){var KA=gA;return function(){var Xi=gA;gA=KA;try{return Bi.apply(this,arguments)}finally{gA=Xi}}}function bt(Bi,KA,Xi){var ia=eA.unstable_now(),pa;if(typeof Xi=="object"&&Xi!==null){var Ha=Xi.delay;typeof Ha=="number"&&Ha>0?pa=ia+Ha:pa=ia}else pa=ia;var Pa;switch(Bi){case lA:Pa=YA;break;case cA:Pa=UA;break;case vA:Pa=Qi;break;case mA:Pa=VA;break;case dA:default:Pa=OA;break}var La=pa+Pa,Qs={id:PA++,callback:KA,priorityLevel:Bi,startTime:pa,expirationTime:La,sortIndex:-1};return pa>ia?(Qs.sortIndex=pa,rA(WA,Qs),iA(jA)===null&&Qs===iA(WA)&&(MA?qA():MA=!0,ts(DA,pa-ia))):(Qs.sortIndex=La,rA(jA,Qs),!wA&&!BA&&(wA=!0,ba(kA))),Qs}function Yi(){}function ta(){!wA&&!BA&&(wA=!0,ba(kA))}function ca(){return iA(jA)}function oa(Bi){Bi.callback=null}function $i(){return gA}var na=!1,Sa=null,wa=-1,Ia=tA,Ra=-1;function Ta(){var Bi=eA.unstable_now()-Ra;return!(Bi125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported");return}Bi>0?Ia=Math.floor(1e3/Bi):Ia=tA}var Na=function(){if(Sa!==null){var Bi=eA.unstable_now();Ra=Bi;var KA=!0,Xi=!0;try{Xi=Sa(KA,Bi)}finally{Xi?ja():(na=!1,Sa=null)}}else na=!1},ja;if(typeof bA=="function")ja=function(){bA(Na)};else if(typeof MessageChannel<"u"){var ya=new MessageChannel,sa=ya.port2;ya.port1.onmessage=Na,ja=function(){sa.postMessage(null)}}else ja=function(){_A(Na,0)};function ba(Bi){Sa=Bi,na||(na=!0,ja())}function ts(Bi,KA){wa=_A(function(){Bi(eA.unstable_now())},KA)}function qA(){TA(wa),wa=-1}var Zi=Ma,da=null;eA.unstable_IdlePriority=vA,eA.unstable_ImmediatePriority=lA,eA.unstable_LowPriority=mA,eA.unstable_NormalPriority=dA,eA.unstable_Profiling=da,eA.unstable_UserBlockingPriority=cA,eA.unstable_cancelCallback=oa,eA.unstable_continueExecution=ta,eA.unstable_forceFrameRate=is,eA.unstable_getCurrentPriorityLevel=$i,eA.unstable_getFirstCallbackNode=ca,eA.unstable_next=aa,eA.unstable_pauseExecution=Yi,eA.unstable_requestPaint=Zi,eA.unstable_runWithPriority=qi,eA.unstable_scheduleCallback=bt,eA.unstable_shouldYield=Ta,eA.unstable_wrapCallback=Fi,typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop=="function"&&__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(new Error)})()})(scheduler_development)),scheduler_development}var hasRequiredScheduler;function requireScheduler(){return hasRequiredScheduler||(hasRequiredScheduler=1,browser$1$1.env.NODE_ENV==="production"?scheduler.exports=requireScheduler_production_min():scheduler.exports=requireScheduler_development()),scheduler.exports}/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var hasRequiredReactDom_production_min;function requireReactDom_production_min(){if(hasRequiredReactDom_production_min)return reactDom_production_min;hasRequiredReactDom_production_min=1;var eA=requireReact(),AA=requireScheduler();function tA(yA){for(var xA="https://reactjs.org/docs/error-decoder.html?invariant="+yA,NA=1;NA"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),uA=Object.prototype.hasOwnProperty,lA=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,cA={},dA={};function mA(yA){return uA.call(dA,yA)?!0:uA.call(cA,yA)?!1:lA.test(yA)?dA[yA]=!0:(cA[yA]=!0,!1)}function vA(yA,xA,NA,JA){if(NA!==null&&NA.type===0)return!1;switch(typeof xA){case"function":case"symbol":return!0;case"boolean":return JA?!1:NA!==null?!NA.acceptsBooleans:(yA=yA.toLowerCase().slice(0,5),yA!=="data-"&&yA!=="aria-");default:return!1}}function EA(yA,xA,NA,JA){if(xA===null||typeof xA>"u"||vA(yA,xA,NA,JA))return!0;if(JA)return!1;if(NA!==null)switch(NA.type){case 3:return!xA;case 4:return xA===!1;case 5:return isNaN(xA);case 6:return isNaN(xA)||1>xA}return!1}function CA(yA,xA,NA,JA,Aa,la,Da){this.acceptsBooleans=xA===2||xA===3||xA===4,this.attributeName=JA,this.attributeNamespace=Aa,this.mustUseProperty=NA,this.propertyName=yA,this.type=xA,this.sanitizeURL=la,this.removeEmptyString=Da}var IA={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(yA){IA[yA]=new CA(yA,0,!1,yA,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(yA){var xA=yA[0];IA[xA]=new CA(xA,1,!1,yA[1],null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(yA){IA[yA]=new CA(yA,2,!1,yA.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(yA){IA[yA]=new CA(yA,2,!1,yA,null,!1,!1)}),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(yA){IA[yA]=new CA(yA,3,!1,yA.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(yA){IA[yA]=new CA(yA,3,!0,yA,null,!1,!1)}),["capture","download"].forEach(function(yA){IA[yA]=new CA(yA,4,!1,yA,null,!1,!1)}),["cols","rows","size","span"].forEach(function(yA){IA[yA]=new CA(yA,6,!1,yA,null,!1,!1)}),["rowSpan","start"].forEach(function(yA){IA[yA]=new CA(yA,5,!1,yA.toLowerCase(),null,!1,!1)});var QA=/[\-:]([a-z])/g;function FA(yA){return yA[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(yA){var xA=yA.replace(QA,FA);IA[xA]=new CA(xA,1,!1,yA,null,!1,!1)}),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(yA){var xA=yA.replace(QA,FA);IA[xA]=new CA(xA,1,!1,yA,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(yA){var xA=yA.replace(QA,FA);IA[xA]=new CA(xA,1,!1,yA,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(yA){IA[yA]=new CA(yA,1,!1,yA.toLowerCase(),null,!1,!1)}),IA.xlinkHref=new CA("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(yA){IA[yA]=new CA(yA,1,!1,yA.toLowerCase(),null,!0,!0)});function GA(yA,xA,NA,JA){var Aa=IA.hasOwnProperty(xA)?IA[xA]:null;(Aa!==null?Aa.type!==0:JA||!(2Wa||Aa[Da]!==la[Wa]){var rs=` +`+Aa[Da].replace(" at new "," at ");return yA.displayName&&rs.includes("")&&(rs=rs.replace("",yA.displayName)),rs}while(1<=Da&&0<=Wa);break}}}finally{$A=!1,Error.prepareStackTrace=NA}return(yA=yA?yA.displayName||yA.name:"")?kA(yA):""}function aa(yA){switch(yA.tag){case 5:return kA(yA.type);case 16:return kA("Lazy");case 13:return kA("Suspense");case 19:return kA("SuspenseList");case 0:case 2:case 15:return yA=qi(yA.type,!1),yA;case 11:return yA=qi(yA.type.render,!1),yA;case 1:return yA=qi(yA.type,!0),yA;default:return""}}function Fi(yA){if(yA==null)return null;if(typeof yA=="function")return yA.displayName||yA.name||null;if(typeof yA=="string")return yA;switch(yA){case VA:return"Fragment";case OA:return"Portal";case jA:return"Profiler";case Qi:return"StrictMode";case gA:return"Suspense";case BA:return"SuspenseList"}if(typeof yA=="object")switch(yA.$$typeof){case PA:return(yA.displayName||"Context")+".Consumer";case WA:return(yA._context.displayName||"Context")+".Provider";case sA:var xA=yA.render;return yA=yA.displayName,yA||(yA=xA.displayName||xA.name||"",yA=yA!==""?"ForwardRef("+yA+")":"ForwardRef"),yA;case wA:return xA=yA.displayName||null,xA!==null?xA:Fi(yA.type)||"Memo";case MA:xA=yA._payload,yA=yA._init;try{return Fi(yA(xA))}catch{}}return null}function bt(yA){var xA=yA.type;switch(yA.tag){case 24:return"Cache";case 9:return(xA.displayName||"Context")+".Consumer";case 10:return(xA._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return yA=xA.render,yA=yA.displayName||yA.name||"",xA.displayName||(yA!==""?"ForwardRef("+yA+")":"ForwardRef");case 7:return"Fragment";case 5:return xA;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Fi(xA);case 8:return xA===Qi?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof xA=="function")return xA.displayName||xA.name||null;if(typeof xA=="string")return xA}return null}function Yi(yA){switch(typeof yA){case"boolean":case"number":case"string":case"undefined":return yA;case"object":return yA;default:return""}}function ta(yA){var xA=yA.type;return(yA=yA.nodeName)&&yA.toLowerCase()==="input"&&(xA==="checkbox"||xA==="radio")}function ca(yA){var xA=ta(yA)?"checked":"value",NA=Object.getOwnPropertyDescriptor(yA.constructor.prototype,xA),JA=""+yA[xA];if(!yA.hasOwnProperty(xA)&&typeof NA<"u"&&typeof NA.get=="function"&&typeof NA.set=="function"){var Aa=NA.get,la=NA.set;return Object.defineProperty(yA,xA,{configurable:!0,get:function(){return Aa.call(this)},set:function(Da){JA=""+Da,la.call(this,Da)}}),Object.defineProperty(yA,xA,{enumerable:NA.enumerable}),{getValue:function(){return JA},setValue:function(Da){JA=""+Da},stopTracking:function(){yA._valueTracker=null,delete yA[xA]}}}}function oa(yA){yA._valueTracker||(yA._valueTracker=ca(yA))}function $i(yA){if(!yA)return!1;var xA=yA._valueTracker;if(!xA)return!0;var NA=xA.getValue(),JA="";return yA&&(JA=ta(yA)?yA.checked?"true":"false":yA.value),yA=JA,yA!==NA?(xA.setValue(yA),!0):!1}function na(yA){if(yA=yA||(typeof document<"u"?document:void 0),typeof yA>"u")return null;try{return yA.activeElement||yA.body}catch{return yA.body}}function Sa(yA,xA){var NA=xA.checked;return hA({},xA,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:NA??yA._wrapperState.initialChecked})}function wa(yA,xA){var NA=xA.defaultValue==null?"":xA.defaultValue,JA=xA.checked!=null?xA.checked:xA.defaultChecked;NA=Yi(xA.value!=null?xA.value:NA),yA._wrapperState={initialChecked:JA,initialValue:NA,controlled:xA.type==="checkbox"||xA.type==="radio"?xA.checked!=null:xA.value!=null}}function Ia(yA,xA){xA=xA.checked,xA!=null&&GA(yA,"checked",xA,!1)}function Ra(yA,xA){Ia(yA,xA);var NA=Yi(xA.value),JA=xA.type;if(NA!=null)JA==="number"?(NA===0&&yA.value===""||yA.value!=NA)&&(yA.value=""+NA):yA.value!==""+NA&&(yA.value=""+NA);else if(JA==="submit"||JA==="reset"){yA.removeAttribute("value");return}xA.hasOwnProperty("value")?Ma(yA,xA.type,NA):xA.hasOwnProperty("defaultValue")&&Ma(yA,xA.type,Yi(xA.defaultValue)),xA.checked==null&&xA.defaultChecked!=null&&(yA.defaultChecked=!!xA.defaultChecked)}function Ta(yA,xA,NA){if(xA.hasOwnProperty("value")||xA.hasOwnProperty("defaultValue")){var JA=xA.type;if(!(JA!=="submit"&&JA!=="reset"||xA.value!==void 0&&xA.value!==null))return;xA=""+yA._wrapperState.initialValue,NA||xA===yA.value||(yA.value=xA),yA.defaultValue=xA}NA=yA.name,NA!==""&&(yA.name=""),yA.defaultChecked=!!yA._wrapperState.initialChecked,NA!==""&&(yA.name=NA)}function Ma(yA,xA,NA){(xA!=="number"||na(yA.ownerDocument)!==yA)&&(NA==null?yA.defaultValue=""+yA._wrapperState.initialValue:yA.defaultValue!==""+NA&&(yA.defaultValue=""+NA))}var is=Array.isArray;function Na(yA,xA,NA,JA){if(yA=yA.options,xA){xA={};for(var Aa=0;Aa"+xA.valueOf().toString()+"",xA=Zi.firstChild;yA.firstChild;)yA.removeChild(yA.firstChild);for(;xA.firstChild;)yA.appendChild(xA.firstChild)}});function Bi(yA,xA){if(xA){var NA=yA.firstChild;if(NA&&NA===yA.lastChild&&NA.nodeType===3){NA.nodeValue=xA;return}}yA.textContent=xA}var KA={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},Xi=["Webkit","ms","Moz","O"];Object.keys(KA).forEach(function(yA){Xi.forEach(function(xA){xA=xA+yA.charAt(0).toUpperCase()+yA.substring(1),KA[xA]=KA[yA]})});function ia(yA,xA,NA){return xA==null||typeof xA=="boolean"||xA===""?"":NA||typeof xA!="number"||xA===0||KA.hasOwnProperty(yA)&&KA[yA]?(""+xA).trim():xA+"px"}function pa(yA,xA){yA=yA.style;for(var NA in xA)if(xA.hasOwnProperty(NA)){var JA=NA.indexOf("--")===0,Aa=ia(NA,xA[NA],JA);NA==="float"&&(NA="cssFloat"),JA?yA.setProperty(NA,Aa):yA[NA]=Aa}}var Ha=hA({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Pa(yA,xA){if(xA){if(Ha[yA]&&(xA.children!=null||xA.dangerouslySetInnerHTML!=null))throw Error(tA(137,yA));if(xA.dangerouslySetInnerHTML!=null){if(xA.children!=null)throw Error(tA(60));if(typeof xA.dangerouslySetInnerHTML!="object"||!("__html"in xA.dangerouslySetInnerHTML))throw Error(tA(61))}if(xA.style!=null&&typeof xA.style!="object")throw Error(tA(62))}}function La(yA,xA){if(yA.indexOf("-")===-1)return typeof xA.is=="string";switch(yA){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Qs=null;function Qa(yA){return yA=yA.target||yA.srcElement||window,yA.correspondingUseElement&&(yA=yA.correspondingUseElement),yA.nodeType===3?yA.parentNode:yA}var za=null,Bs=null,Xa=null;function Ka(yA){if(yA=gu(yA)){if(typeof za!="function")throw Error(tA(280));var xA=yA.stateNode;xA&&(xA=Zl(xA),za(yA.stateNode,yA.type,xA))}}function vs(yA){Bs?Xa?Xa.push(yA):Xa=[yA]:Bs=yA}function As(){if(Bs){var yA=Bs,xA=Xa;if(Xa=Bs=null,Ka(yA),xA)for(yA=0;yA>>=0,yA===0?32:31-(al(yA)/Bl|0)|0}var gl=64,ku=4194304;function Ul(yA){switch(yA&-yA){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return yA&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return yA&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return yA}}function uc(yA,xA){var NA=yA.pendingLanes;if(NA===0)return 0;var JA=0,Aa=yA.suspendedLanes,la=yA.pingedLanes,Da=NA&268435455;if(Da!==0){var Wa=Da&~Aa;Wa!==0?JA=Ul(Wa):(la&=Da,la!==0&&(JA=Ul(la)))}else Da=NA&~Aa,Da!==0?JA=Ul(Da):la!==0&&(JA=Ul(la));if(JA===0)return 0;if(xA!==0&&xA!==JA&&(xA&Aa)===0&&(Aa=JA&-JA,la=xA&-xA,Aa>=la||Aa===16&&(la&4194240)!==0))return xA;if((JA&4)!==0&&(JA|=NA&16),xA=yA.entangledLanes,xA!==0)for(yA=yA.entanglements,xA&=JA;0NA;NA++)xA.push(yA);return xA}function zl(yA,xA,NA){yA.pendingLanes|=xA,xA!==536870912&&(yA.suspendedLanes=0,yA.pingedLanes=0),yA=yA.eventTimes,xA=31-Ju(xA),yA[xA]=NA}function Dc(yA,xA){var NA=yA.pendingLanes&~xA;yA.pendingLanes=xA,yA.suspendedLanes=0,yA.pingedLanes=0,yA.expiredLanes&=xA,yA.mutableReadLanes&=xA,yA.entangledLanes&=xA,xA=yA.entanglements;var JA=yA.eventTimes;for(yA=yA.expirationTimes;0=Ig),dE=" ",tm=!1;function pE(yA,xA){switch(yA){case"keyup":return df.indexOf(xA.keyCode)!==-1;case"keydown":return xA.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function BE(yA){return yA=yA.detail,typeof yA=="object"&&"data"in yA?yA.data:null}var Gp=!1;function jd(yA,xA){switch(yA){case"compositionend":return BE(xA);case"keypress":return xA.which!==32?null:(tm=!0,dE);case"textInput":return yA=xA.data,yA===dE&&tm?null:yA;default:return null}}function kw(yA,xA){if(Gp)return yA==="compositionend"||!rd&&pE(yA,xA)?(yA=Ms(),qa=va=Rl=null,Gp=!1,yA):null;switch(yA){case"paste":return null;case"keypress":if(!(xA.ctrlKey||xA.altKey||xA.metaKey)||xA.ctrlKey&&xA.altKey){if(xA.char&&1=xA)return{node:NA,offset:xA-yA};yA=JA}A:{for(;NA;){if(NA.nextSibling){NA=NA.nextSibling;break A}NA=NA.parentNode}NA=void 0}NA=Ou(NA)}}function Ll(yA,xA){return yA&&xA?yA===xA?!0:yA&&yA.nodeType===3?!1:xA&&xA.nodeType===3?Ll(yA,xA.parentNode):"contains"in yA?yA.contains(xA):yA.compareDocumentPosition?!!(yA.compareDocumentPosition(xA)&16):!1:!1}function Ml(){for(var yA=window,xA=na();xA instanceof yA.HTMLIFrameElement;){try{var NA=typeof xA.contentWindow.location.href=="string"}catch{NA=!1}if(NA)yA=xA.contentWindow;else break;xA=na(yA.document)}return xA}function Lp(yA){var xA=yA&&yA.nodeName&&yA.nodeName.toLowerCase();return xA&&(xA==="input"&&(yA.type==="text"||yA.type==="search"||yA.type==="tel"||yA.type==="url"||yA.type==="password")||xA==="textarea"||yA.contentEditable==="true")}function Hp(yA){var xA=Ml(),NA=yA.focusedElem,JA=yA.selectionRange;if(xA!==NA&&NA&&NA.ownerDocument&&Ll(NA.ownerDocument.documentElement,NA)){if(JA!==null&&Lp(NA)){if(xA=JA.start,yA=JA.end,yA===void 0&&(yA=xA),"selectionStart"in NA)NA.selectionStart=xA,NA.selectionEnd=Math.min(yA,NA.value.length);else if(yA=(xA=NA.ownerDocument||document)&&xA.defaultView||window,yA.getSelection){yA=yA.getSelection();var Aa=NA.textContent.length,la=Math.min(JA.start,Aa);JA=JA.end===void 0?la:Math.min(JA.end,Aa),!yA.extend&&la>JA&&(Aa=JA,JA=la,la=Aa),Aa=dl(NA,la);var Da=dl(NA,JA);Aa&&Da&&(yA.rangeCount!==1||yA.anchorNode!==Aa.node||yA.anchorOffset!==Aa.offset||yA.focusNode!==Da.node||yA.focusOffset!==Da.offset)&&(xA=xA.createRange(),xA.setStart(Aa.node,Aa.offset),yA.removeAllRanges(),la>JA?(yA.addRange(xA),yA.extend(Da.node,Da.offset)):(xA.setEnd(Da.node,Da.offset),yA.addRange(xA)))}}for(xA=[],yA=NA;yA=yA.parentNode;)yA.nodeType===1&&xA.push({element:yA,left:yA.scrollLeft,top:yA.scrollTop});for(typeof NA.focus=="function"&&NA.focus(),NA=0;NA=document.documentMode,Mg=null,Cv=null,ig=null,kp=!1;function Np(yA,xA,NA){var JA=NA.window===NA?NA.document:NA.nodeType===9?NA:NA.ownerDocument;kp||Mg==null||Mg!==na(JA)||(JA=Mg,"selectionStart"in JA&&Lp(JA)?JA={start:JA.selectionStart,end:JA.selectionEnd}:(JA=(JA.ownerDocument&&JA.ownerDocument.defaultView||window).getSelection(),JA={anchorNode:JA.anchorNode,anchorOffset:JA.anchorOffset,focusNode:JA.focusNode,focusOffset:JA.focusOffset}),ig&&su(ig,JA)||(ig=JA,JA=WB(Cv,"onSelect"),0hh||(yA.current=ll[hh],ll[hh]=null,hh--)}function Cu(yA,xA){hh++,ll[hh]=yA.current,yA.current=xA}var tf={},nc=Lc(tf),xc=Lc(!1),ic=tf;function Ff(yA,xA){var NA=yA.type.contextTypes;if(!NA)return tf;var JA=yA.stateNode;if(JA&&JA.__reactInternalMemoizedUnmaskedChildContext===xA)return JA.__reactInternalMemoizedMaskedChildContext;var Aa={},la;for(la in NA)Aa[la]=xA[la];return JA&&(yA=yA.stateNode,yA.__reactInternalMemoizedUnmaskedChildContext=xA,yA.__reactInternalMemoizedMaskedChildContext=Aa),Aa}function wc(yA){return yA=yA.childContextTypes,yA!=null}function Vp(){Hl(xc),Hl(nc)}function bE(yA,xA,NA){if(nc.current!==tf)throw Error(tA(168));Cu(nc,xA),Cu(xc,NA)}function KB(yA,xA,NA){var JA=yA.stateNode;if(xA=xA.childContextTypes,typeof JA.getChildContext!="function")return NA;JA=JA.getChildContext();for(var Aa in JA)if(!(Aa in xA))throw Error(tA(108,bt(yA)||"Unknown",Aa));return hA({},NA,JA)}function kl(yA){return yA=(yA=yA.stateNode)&&yA.__reactInternalMemoizedMergedChildContext||tf,ic=nc.current,Cu(nc,yA),Cu(xc,xc.current),!0}function fm(yA,xA,NA){var JA=yA.stateNode;if(!JA)throw Error(tA(169));NA?(yA=KB(yA,xA,ic),JA.__reactInternalMemoizedMergedChildContext=yA,Hl(xc),Hl(nc),Cu(nc,yA)):Hl(xc),Cu(xc,NA)}var ug=null,qp=!1,Yg=!1;function hm(yA){ug===null?ug=[yA]:ug.push(yA)}function Yf(yA){qp=!0,hm(yA)}function lg(){if(!Yg&&ug!==null){Yg=!0;var yA=0,xA=Ku;try{var NA=ug;for(Ku=1;yA>=Da,Aa-=Da,Gh=1<<32-Ju(xA)+Aa|NA<Gu?(kc=bu,bu=null):kc=bu.sibling;var Il=Hs(ms,bu,ws[Gu],ks);if(Il===null){bu===null&&(bu=kc);break}yA&&bu&&Il.alternate===null&&xA(ms,bu),as=la(Il,as,Gu),Pu===null?Bu=Il:Pu.sibling=Il,Pu=Il,bu=kc}if(Gu===ws.length)return NA(ms,bu),$l&&Ap(ms,Gu),Bu;if(bu===null){for(;GuGu?(kc=bu,bu=null):kc=bu.sibling;var Pd=Hs(ms,bu,Il.value,ks);if(Pd===null){bu===null&&(bu=kc);break}yA&&bu&&Pd.alternate===null&&xA(ms,bu),as=la(Pd,as,Gu),Pu===null?Bu=Pd:Pu.sibling=Pd,Pu=Pd,bu=kc}if(Il.done)return NA(ms,bu),$l&&Ap(ms,Gu),Bu;if(bu===null){for(;!Il.done;Gu++,Il=ws.next())Il=$s(ms,Il.value,ks),Il!==null&&(as=la(Il,as,Gu),Pu===null?Bu=Il:Pu.sibling=Il,Pu=Il);return $l&&Ap(ms,Gu),Bu}for(bu=JA(ms,bu);!Il.done;Gu++,Il=ws.next())Il=uu(bu,ms,Gu,Il.value,ks),Il!==null&&(yA&&Il.alternate!==null&&bu.delete(Il.key===null?Gu:Il.key),as=la(Il,as,Gu),Pu===null?Bu=Il:Pu.sibling=Il,Pu=Il);return yA&&bu.forEach(function(ew){return xA(ms,ew)}),$l&&Ap(ms,Gu),Bu}function dc(ms,as,ws,ks){if(typeof ws=="object"&&ws!==null&&ws.type===VA&&ws.key===null&&(ws=ws.props.children),typeof ws=="object"&&ws!==null){switch(ws.$$typeof){case UA:A:{for(var Bu=ws.key,Pu=as;Pu!==null;){if(Pu.key===Bu){if(Bu=ws.type,Bu===VA){if(Pu.tag===7){NA(ms,Pu.sibling),as=Aa(Pu,ws.props.children),as.return=ms,ms=as;break A}}else if(Pu.elementType===Bu||typeof Bu=="object"&&Bu!==null&&Bu.$$typeof===MA&&SE(Bu)===Pu.type){NA(ms,Pu.sibling),as=Aa(Pu,ws.props),as.ref=ep(ms,Pu,ws),as.return=ms,ms=as;break A}NA(ms,Pu);break}else xA(ms,Pu);Pu=Pu.sibling}ws.type===VA?(as=Kg(ws.props.children,ms.mode,ks,ws.key),as.return=ms,ms=as):(ks=I0(ws.type,ws.key,ws.props,null,ms.mode,ks),ks.ref=ep(ms,as,ws),ks.return=ms,ms=ks)}return Da(ms);case OA:A:{for(Pu=ws.key;as!==null;){if(as.key===Pu)if(as.tag===4&&as.stateNode.containerInfo===ws.containerInfo&&as.stateNode.implementation===ws.implementation){NA(ms,as.sibling),as=Aa(as,ws.children||[]),as.return=ms,ms=as;break A}else{NA(ms,as);break}else xA(ms,as);as=as.sibling}as=Um(ws,ms.mode,ks),as.return=ms,ms=as}return Da(ms);case MA:return Pu=ws._init,dc(ms,as,Pu(ws._payload),ks)}if(is(ws))return pu(ms,as,ws,ks);if(bA(ws))return mu(ms,as,ws,ks);tp(ms,ws)}return typeof ws=="string"&&ws!==""||typeof ws=="number"?(ws=""+ws,as!==null&&as.tag===6?(NA(ms,as.sibling),as=Aa(as,ws),as.return=ms,ms=as):(NA(ms,as),as=Uv(ws,ms.mode,ks),as.return=ms,ms=as),Da(ms)):NA(ms,as)}return dc}var rl=hd(!0),nu=hd(!1),Gf=Lc(null),Ac=null,gd=null,AB=null;function Gg(){AB=gd=Ac=null}function dm(yA){var xA=Gf.current;Hl(Gf),yA._currentValue=xA}function Wc(yA,xA,NA){for(;yA!==null;){var JA=yA.alternate;if((yA.childLanes&xA)!==xA?(yA.childLanes|=xA,JA!==null&&(JA.childLanes|=xA)):JA!==null&&(JA.childLanes&xA)!==xA&&(JA.childLanes|=xA),yA===NA)break;yA=yA.return}}function Wl(yA,xA){Ac=yA,AB=gd=null,yA=yA.dependencies,yA!==null&&yA.firstContext!==null&&((yA.lanes&xA)!==0&&(hc=!0),yA.firstContext=null)}function mh(yA){var xA=yA._currentValue;if(AB!==yA)if(yA={context:yA,memoizedValue:xA,next:null},gd===null){if(Ac===null)throw Error(tA(308));gd=yA,Ac.dependencies={lanes:0,firstContext:yA}}else gd=gd.next=yA;return xA}var rp=null;function yv(yA){rp===null?rp=[yA]:rp.push(yA)}function qB(yA,xA,NA,JA){var Aa=xA.interleaved;return Aa===null?(NA.next=NA,yv(xA)):(NA.next=Aa.next,Aa.next=NA),xA.interleaved=NA,Jf(yA,JA)}function Jf(yA,xA){yA.lanes|=xA;var NA=yA.alternate;for(NA!==null&&(NA.lanes|=xA),NA=yA,yA=yA.return;yA!==null;)yA.childLanes|=xA,NA=yA.alternate,NA!==null&&(NA.childLanes|=xA),NA=yA,yA=yA.return;return NA.tag===3?NA.stateNode:null}var Jc=!1;function pm(yA){yA.updateQueue={baseState:yA.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function _E(yA,xA){yA=yA.updateQueue,xA.updateQueue===yA&&(xA.updateQueue={baseState:yA.baseState,firstBaseUpdate:yA.firstBaseUpdate,lastBaseUpdate:yA.lastBaseUpdate,shared:yA.shared,effects:yA.effects})}function Ug(yA,xA){return{eventTime:yA,lane:xA,tag:0,payload:null,callback:null,next:null}}function vh(yA,xA,NA){var JA=yA.updateQueue;if(JA===null)return null;if(JA=JA.shared,(cl&2)!==0){var Aa=JA.pending;return Aa===null?xA.next=xA:(xA.next=Aa.next,Aa.next=xA),JA.pending=xA,Jf(yA,NA)}return Aa=JA.interleaved,Aa===null?(xA.next=xA,yv(JA)):(xA.next=Aa.next,Aa.next=xA),JA.interleaved=xA,Jf(yA,NA)}function Bm(yA,xA,NA){if(xA=xA.updateQueue,xA!==null&&(xA=xA.shared,(NA&4194240)!==0)){var JA=xA.lanes;JA&=yA.pendingLanes,NA|=JA,xA.lanes=NA,ff(yA,NA)}}function FE(yA,xA){var NA=yA.updateQueue,JA=yA.alternate;if(JA!==null&&(JA=JA.updateQueue,NA===JA)){var Aa=null,la=null;if(NA=NA.firstBaseUpdate,NA!==null){do{var Da={eventTime:NA.eventTime,lane:NA.lane,tag:NA.tag,payload:NA.payload,callback:NA.callback,next:null};la===null?Aa=la=Da:la=la.next=Da,NA=NA.next}while(NA!==null);la===null?Aa=la=xA:la=la.next=xA}else Aa=la=xA;NA={baseState:JA.baseState,firstBaseUpdate:Aa,lastBaseUpdate:la,shared:JA.shared,effects:JA.effects},yA.updateQueue=NA;return}yA=NA.lastBaseUpdate,yA===null?NA.firstBaseUpdate=xA:yA.next=xA,NA.lastBaseUpdate=xA}function np(yA,xA,NA,JA){var Aa=yA.updateQueue;Jc=!1;var la=Aa.firstBaseUpdate,Da=Aa.lastBaseUpdate,Wa=Aa.shared.pending;if(Wa!==null){Aa.shared.pending=null;var rs=Wa,Ds=rs.next;rs.next=null,Da===null?la=Ds:Da.next=Ds,Da=rs;var zs=yA.alternate;zs!==null&&(zs=zs.updateQueue,Wa=zs.lastBaseUpdate,Wa!==Da&&(Wa===null?zs.firstBaseUpdate=Ds:Wa.next=Ds,zs.lastBaseUpdate=rs))}if(la!==null){var $s=Aa.baseState;Da=0,zs=Ds=rs=null,Wa=la;do{var Hs=Wa.lane,uu=Wa.eventTime;if((JA&Hs)===Hs){zs!==null&&(zs=zs.next={eventTime:uu,lane:0,tag:Wa.tag,payload:Wa.payload,callback:Wa.callback,next:null});A:{var pu=yA,mu=Wa;switch(Hs=xA,uu=NA,mu.tag){case 1:if(pu=mu.payload,typeof pu=="function"){$s=pu.call(uu,$s,Hs);break A}$s=pu;break A;case 3:pu.flags=pu.flags&-65537|128;case 0:if(pu=mu.payload,Hs=typeof pu=="function"?pu.call(uu,$s,Hs):pu,Hs==null)break A;$s=hA({},$s,Hs);break A;case 2:Jc=!0}}Wa.callback!==null&&Wa.lane!==0&&(yA.flags|=64,Hs=Aa.effects,Hs===null?Aa.effects=[Wa]:Hs.push(Wa))}else uu={eventTime:uu,lane:Hs,tag:Wa.tag,payload:Wa.payload,callback:Wa.callback,next:null},zs===null?(Ds=zs=uu,rs=$s):zs=zs.next=uu,Da|=Hs;if(Wa=Wa.next,Wa===null){if(Wa=Aa.shared.pending,Wa===null)break;Hs=Wa,Wa=Hs.next,Hs.next=null,Aa.lastBaseUpdate=Hs,Aa.shared.pending=null}}while(!0);if(zs===null&&(rs=$s),Aa.baseState=rs,Aa.firstBaseUpdate=Ds,Aa.lastBaseUpdate=zs,xA=Aa.shared.interleaved,xA!==null){Aa=xA;do Da|=Aa.lane,Aa=Aa.next;while(Aa!==xA)}else la===null&&(Aa.shared.lanes=0);zh|=Da,yA.lanes=Da,yA.memoizedState=$s}}function mm(yA,xA,NA){if(yA=xA.effects,xA.effects=null,yA!==null)for(xA=0;xANA?NA:4,yA(!0);var JA=yl.transition;yl.transition={};try{yA(!1),xA()}finally{Ku=NA,yl.transition=JA}}function Cm(){return fc().memoizedState}function s0(yA,xA,NA){var JA=pg(yA);if(NA={lane:JA,action:NA,hasEagerState:!1,eagerState:null,next:null},Bf(yA))mf(xA,NA);else if(NA=qB(yA,xA,NA,JA),NA!==null){var Aa=Mc();Cf(NA,yA,JA,Aa),ol(NA,xA,JA)}}function vd(yA,xA,NA){var JA=pg(yA),Aa={lane:JA,action:NA,hasEagerState:!1,eagerState:null,next:null};if(Bf(yA))mf(xA,Aa);else{var la=yA.alternate;if(yA.lanes===0&&(la===null||la.lanes===0)&&(la=xA.lastRenderedReducer,la!==null))try{var Da=xA.lastRenderedState,Wa=la(Da,NA);if(Aa.hasEagerState=!0,Aa.eagerState=Wa,Is(Wa,Da)){var rs=xA.interleaved;rs===null?(Aa.next=Aa,yv(xA)):(Aa.next=rs.next,rs.next=Aa),xA.interleaved=Aa;return}}catch{}finally{}NA=qB(yA,xA,Aa,JA),NA!==null&&(Aa=Mc(),Cf(NA,yA,JA,Aa),ol(NA,xA,JA))}}function Bf(yA){var xA=yA.alternate;return yA===Ql||xA!==null&&xA===Ql}function mf(yA,xA){r0=tB=!0;var NA=yA.pending;NA===null?xA.next=xA:(xA.next=NA.next,NA.next=xA),yA.pending=xA}function ol(yA,xA,NA){if((NA&4194240)!==0){var JA=xA.lanes;JA&=yA.pendingLanes,NA|=JA,xA.lanes=NA,ff(yA,NA)}}var up={readContext:mh,useCallback:El,useContext:El,useEffect:El,useImperativeHandle:El,useInsertionEffect:El,useLayoutEffect:El,useMemo:El,useReducer:El,useRef:El,useState:El,useDebugValue:El,useDeferredValue:El,useTransition:El,useMutableSource:El,useSyncExternalStore:El,useId:El,unstable_isNewReconciler:!1},ym={readContext:mh,useCallback:function(yA,xA){return nf().memoizedState=[yA,xA===void 0?null:xA],yA},useContext:mh,useEffect:oB,useImperativeHandle:function(yA,xA,NA){return NA=NA!=null?NA.concat([yA]):null,ap(4194308,4,md.bind(null,xA,yA),NA)},useLayoutEffect:function(yA,xA){return ap(4194308,4,yA,xA)},useInsertionEffect:function(yA,xA){return ap(4,2,yA,xA)},useMemo:function(yA,xA){var NA=nf();return xA=xA===void 0?null:xA,yA=yA(),NA.memoizedState=[yA,xA],yA},useReducer:function(yA,xA,NA){var JA=nf();return xA=NA!==void 0?NA(xA):xA,JA.memoizedState=JA.baseState=xA,yA={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:yA,lastRenderedState:xA},JA.queue=yA,yA=yA.dispatch=s0.bind(null,Ql,yA),[JA.memoizedState,yA]},useRef:function(yA){var xA=nf();return yA={current:yA},xA.memoizedState=yA},useState:op,useDebugValue:sp,useDeferredValue:function(yA){return nf().memoizedState=yA},useTransition:function(){var yA=op(!1),xA=yA[0];return yA=aB.bind(null,yA[1]),nf().memoizedState=yA,[xA,yA]},useMutableSource:function(){},useSyncExternalStore:function(yA,xA,NA){var JA=Ql,Aa=nf();if($l){if(NA===void 0)throw Error(tA(407));NA=NA()}else{if(NA=xA(),Yc===null)throw Error(tA(349));(Fl&30)!==0||i0(JA,xA,NA)}Aa.memoizedState=NA;var la={value:NA,getSnapshot:xA};return Aa.queue=la,oB(Hh.bind(null,JA,la,yA),[yA]),JA.flags|=2048,pd(9,of.bind(null,JA,la,NA,xA),void 0,null),NA},useId:function(){var yA=nf(),xA=Yc.identifierPrefix;if($l){var NA=Uh,JA=Gh;NA=(JA&~(1<<32-Ju(JA)-1)).toString(32)+NA,xA=":"+xA+"R"+NA,NA=kg++,0<\/script>",yA=yA.removeChild(yA.firstChild)):typeof JA.is=="string"?yA=Da.createElement(NA,{is:JA.is}):(yA=Da.createElement(NA),NA==="select"&&(Da=yA,JA.multiple?Da.multiple=!0:JA.size&&(Da.size=JA.size))):yA=Da.createElementNS(yA,NA),yA[ag]=xA,yA[ud]=JA,Gl(yA,xA,!1,!1),xA.stateNode=yA;A:{switch(Da=La(NA,JA),NA){case"dialog":xl("cancel",yA),xl("close",yA),Aa=JA;break;case"iframe":case"object":case"embed":xl("load",yA),Aa=JA;break;case"video":case"audio":for(Aa=0;Aafp&&(xA.flags|=128,JA=!0,d0(la,!1),xA.lanes=4194304)}else{if(!JA)if(yA=ip(Da),yA!==null){if(xA.flags|=128,JA=!0,NA=yA.updateQueue,NA!==null&&(xA.updateQueue=NA,xA.flags|=4),d0(la,!0),la.tail===null&&la.tailMode==="hidden"&&!Da.alternate&&!$l)return Fc(xA),null}else 2*Ja()-la.renderingStartTime>fp&&NA!==1073741824&&(xA.flags|=128,JA=!0,d0(la,!1),xA.lanes=4194304);la.isBackwards?(Da.sibling=xA.child,xA.child=Da):(NA=la.last,NA!==null?NA.sibling=Da:xA.child=Da,la.last=Da)}return la.tail!==null?(xA=la.tail,la.rendering=xA,la.tail=xA.sibling,la.renderingStartTime=Ja(),xA.sibling=null,NA=Kl.current,Cu(Kl,JA?NA&1|2:NA&1),xA):(Fc(xA),null);case 22:case 23:return Tv(),JA=xA.memoizedState!==null,yA!==null&&yA.memoizedState!==null!==JA&&(xA.flags|=8192),JA&&(xA.mode&1)!==0?(Vf&1073741824)!==0&&(Fc(xA),xA.subtreeFlags&6&&(xA.flags|=8192)):Fc(xA),null;case 24:return null;case 25:return null}throw Error(tA(156,xA.tag))}function bm(yA,xA){switch(fd(xA),xA.tag){case 1:return wc(xA.type)&&Vp(),yA=xA.flags,yA&65536?(xA.flags=yA&-65537|128,xA):null;case 3:return Hg(),Hl(xc),Hl(nc),Qu(),yA=xA.flags,(yA&65536)!==0&&(yA&128)===0?(xA.flags=yA&-65537|128,xA):null;case 5:return A0(xA),null;case 13:if(Hl(Kl),yA=xA.memoizedState,yA!==null&&yA.dehydrated!==null){if(xA.alternate===null)throw Error(tA(340));Bh()}return yA=xA.flags,yA&65536?(xA.flags=yA&-65537|128,xA):null;case 19:return Hl(Kl),null;case 4:return Hg(),null;case 10:return dm(xA.type._context),null;case 22:case 23:return Tv(),null;case 24:return null;default:return null}}var B0=!1,Vl=!1,vf=typeof WeakSet=="function"?WeakSet:Set,cu=null;function mB(yA,xA){var NA=yA.ref;if(NA!==null)if(typeof NA=="function")try{NA(null)}catch(JA){Jl(yA,xA,JA)}else NA.current=null}function vB(yA,xA,NA){try{NA()}catch(JA){Jl(yA,xA,JA)}}var UE=!1;function LE(yA,xA){if(Xd=uh,yA=Ml(),Lp(yA)){if("selectionStart"in yA)var NA={start:yA.selectionStart,end:yA.selectionEnd};else A:{NA=(NA=yA.ownerDocument)&&NA.defaultView||window;var JA=NA.getSelection&&NA.getSelection();if(JA&&JA.rangeCount!==0){NA=JA.anchorNode;var Aa=JA.anchorOffset,la=JA.focusNode;JA=JA.focusOffset;try{NA.nodeType,la.nodeType}catch{NA=null;break A}var Da=0,Wa=-1,rs=-1,Ds=0,zs=0,$s=yA,Hs=null;e:for(;;){for(var uu;$s!==NA||Aa!==0&&$s.nodeType!==3||(Wa=Da+Aa),$s!==la||JA!==0&&$s.nodeType!==3||(rs=Da+JA),$s.nodeType===3&&(Da+=$s.nodeValue.length),(uu=$s.firstChild)!==null;)Hs=$s,$s=uu;for(;;){if($s===yA)break e;if(Hs===NA&&++Ds===Aa&&(Wa=Da),Hs===la&&++zs===JA&&(rs=Da),(uu=$s.nextSibling)!==null)break;$s=Hs,Hs=$s.parentNode}$s=uu}NA=Wa===-1||rs===-1?null:{start:Wa,end:rs}}else NA=null}NA=NA||{start:0,end:0}}else NA=null;for(Th={focusedElem:yA,selectionRange:NA},uh=!1,cu=xA;cu!==null;)if(xA=cu,yA=xA.child,(xA.subtreeFlags&1028)!==0&&yA!==null)yA.return=xA,cu=yA;else for(;cu!==null;){xA=cu;try{var pu=xA.alternate;if((xA.flags&1024)!==0)switch(xA.tag){case 0:case 11:case 15:break;case 1:if(pu!==null){var mu=pu.memoizedProps,dc=pu.memoizedState,ms=xA.stateNode,as=ms.getSnapshotBeforeUpdate(xA.elementType===xA.type?mu:Qh(xA.type,mu),dc);ms.__reactInternalSnapshotBeforeUpdate=as}break;case 3:var ws=xA.stateNode.containerInfo;ws.nodeType===1?ws.textContent="":ws.nodeType===9&&ws.documentElement&&ws.removeChild(ws.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(tA(163))}}catch(ks){Jl(xA,xA.return,ks)}if(yA=xA.sibling,yA!==null){yA.return=xA.return,cu=yA;break}cu=xA.return}return pu=UE,UE=!1,pu}function jg(yA,xA,NA){var JA=xA.updateQueue;if(JA=JA!==null?JA.lastEffect:null,JA!==null){var Aa=JA=JA.next;do{if((Aa.tag&yA)===yA){var la=Aa.destroy;Aa.destroy=void 0,la!==void 0&&vB(xA,NA,la)}Aa=Aa.next}while(Aa!==JA)}}function wB(yA,xA){if(xA=xA.updateQueue,xA=xA!==null?xA.lastEffect:null,xA!==null){var NA=xA=xA.next;do{if((NA.tag&yA)===yA){var JA=NA.create;NA.destroy=JA()}NA=NA.next}while(NA!==xA)}}function xm(yA){var xA=yA.ref;if(xA!==null){var NA=yA.stateNode;switch(yA.tag){case 5:yA=NA;break;default:yA=NA}typeof xA=="function"?xA(yA):xA.current=yA}}function Pm(yA){var xA=yA.alternate;xA!==null&&(yA.alternate=null,Pm(xA)),yA.child=null,yA.deletions=null,yA.sibling=null,yA.tag===5&&(xA=yA.stateNode,xA!==null&&(delete xA[ag],delete xA[ud],delete xA[Jp],delete xA[_a],delete xA[Kp])),yA.stateNode=null,yA.return=null,yA.dependencies=null,yA.memoizedProps=null,yA.memoizedState=null,yA.pendingProps=null,yA.stateNode=null,yA.updateQueue=null}function m0(yA){return yA.tag===5||yA.tag===3||yA.tag===4}function $g(yA){A:for(;;){for(;yA.sibling===null;){if(yA.return===null||m0(yA.return))return null;yA=yA.return}for(yA.sibling.return=yA.return,yA=yA.sibling;yA.tag!==5&&yA.tag!==6&&yA.tag!==18;){if(yA.flags&2||yA.child===null||yA.tag===4)continue A;yA.child.return=yA,yA=yA.child}if(!(yA.flags&2))return yA.stateNode}}function dg(yA,xA,NA){var JA=yA.tag;if(JA===5||JA===6)yA=yA.stateNode,xA?NA.nodeType===8?NA.parentNode.insertBefore(yA,xA):NA.insertBefore(yA,xA):(NA.nodeType===8?(xA=NA.parentNode,xA.insertBefore(yA,NA)):(xA=NA,xA.appendChild(yA)),NA=NA._reactRootContainer,NA!=null||xA.onclick!==null||(xA.onclick=ad));else if(JA!==4&&(yA=yA.child,yA!==null))for(dg(yA,xA,NA),yA=yA.sibling;yA!==null;)dg(yA,xA,NA),yA=yA.sibling}function kh(yA,xA,NA){var JA=yA.tag;if(JA===5||JA===6)yA=yA.stateNode,xA?NA.insertBefore(yA,xA):NA.appendChild(yA);else if(JA!==4&&(yA=yA.child,yA!==null))for(kh(yA,xA,NA),yA=yA.sibling;yA!==null;)kh(yA,xA,NA),yA=yA.sibling}var ac=null,Ef=!1;function Vc(yA,xA,NA){for(NA=NA.child;NA!==null;)Pv(yA,xA,NA),NA=NA.sibling}function Pv(yA,xA,NA){if(Au&&typeof Au.onCommitFiberUnmount=="function")try{Au.onCommitFiberUnmount(Vs,NA)}catch{}switch(NA.tag){case 5:Vl||mB(NA,xA);case 6:var JA=ac,Aa=Ef;ac=null,Vc(yA,xA,NA),ac=JA,Ef=Aa,ac!==null&&(Ef?(yA=ac,NA=NA.stateNode,yA.nodeType===8?yA.parentNode.removeChild(NA):yA.removeChild(NA)):ac.removeChild(NA.stateNode));break;case 18:ac!==null&&(Ef?(yA=ac,NA=NA.stateNode,yA.nodeType===8?Wp(yA.parentNode,NA):yA.nodeType===1&&Wp(yA,NA),il(yA)):Wp(ac,NA.stateNode));break;case 4:JA=ac,Aa=Ef,ac=NA.stateNode.containerInfo,Ef=!0,Vc(yA,xA,NA),ac=JA,Ef=Aa;break;case 0:case 11:case 14:case 15:if(!Vl&&(JA=NA.updateQueue,JA!==null&&(JA=JA.lastEffect,JA!==null))){Aa=JA=JA.next;do{var la=Aa,Da=la.destroy;la=la.tag,Da!==void 0&&((la&2)!==0||(la&4)!==0)&&vB(NA,xA,Da),Aa=Aa.next}while(Aa!==JA)}Vc(yA,xA,NA);break;case 1:if(!Vl&&(mB(NA,xA),JA=NA.stateNode,typeof JA.componentWillUnmount=="function"))try{JA.props=NA.memoizedProps,JA.state=NA.memoizedState,JA.componentWillUnmount()}catch(Wa){Jl(NA,xA,Wa)}Vc(yA,xA,NA);break;case 21:Vc(yA,xA,NA);break;case 22:NA.mode&1?(Vl=(JA=Vl)||NA.memoizedState!==null,Vc(yA,xA,NA),Vl=JA):Vc(yA,xA,NA);break;default:Vc(yA,xA,NA)}}function CB(yA){var xA=yA.updateQueue;if(xA!==null){yA.updateQueue=null;var NA=yA.stateNode;NA===null&&(NA=yA.stateNode=new vf),xA.forEach(function(JA){var Aa=WE.bind(null,yA,JA);NA.has(JA)||(NA.add(JA),JA.then(Aa,Aa))})}}function Kf(yA,xA){var NA=xA.deletions;if(NA!==null)for(var JA=0;JAAa&&(Aa=Da),JA&=~la}if(JA=Aa,JA=Ja()-JA,JA=(120>JA?120:480>JA?480:1080>JA?1080:1920>JA?1920:3e3>JA?3e3:4320>JA?4320:1960*HE(JA/1960))-JA,10yA?16:yA,Wg===null)var JA=!1;else{if(yA=Wg,Wg=null,sf=0,(cl&6)!==0)throw Error(tA(331));var Aa=cl;for(cl|=4,cu=yA.current;cu!==null;){var la=cu,Da=la.child;if((cu.flags&16)!==0){var Wa=la.deletions;if(Wa!==null){for(var rs=0;rsJa()-Fv?Jg(yA,0):E0|=NA),kf(yA,xA)}function $E(yA,xA){xA===0&&((yA.mode&1)===0?xA=1:(xA=ku,ku<<=1,(ku&130023424)===0&&(ku=4194304)));var NA=Mc();yA=Jf(yA,xA),yA!==null&&(zl(yA,xA,NA),kf(yA,NA))}function Vw(yA){var xA=yA.memoizedState,NA=0;xA!==null&&(NA=xA.retryLane),$E(yA,NA)}function WE(yA,xA){var NA=0;switch(yA.tag){case 13:var JA=yA.stateNode,Aa=yA.memoizedState;Aa!==null&&(NA=Aa.retryLane);break;case 19:JA=yA.stateNode;break;default:throw Error(tA(314))}JA!==null&&JA.delete(xA),$E(yA,NA)}var JE;JE=function(yA,xA,NA){if(yA!==null)if(yA.memoizedProps!==xA.pendingProps||xc.current)hc=!0;else{if((yA.lanes&NA)===0&&(xA.flags&128)===0)return hc=!1,BB(yA,xA,NA);hc=(yA.flags&131072)!==0}else hc=!1,$l&&(xA.flags&1048576)!==0&&xE(xA,Rg,xA.index);switch(xA.lanes=0,xA.tag){case 2:var JA=xA.type;Ih(yA,xA),yA=xA.pendingProps;var Aa=Ff(xA,nc.current);Wl(xA,NA),Aa=Eh(null,xA,JA,yA,Aa,NA);var la=wh();return xA.flags|=1,typeof Aa=="object"&&Aa!==null&&typeof Aa.render=="function"&&Aa.$$typeof===void 0?(xA.tag=1,xA.memoizedState=null,xA.updateQueue=null,wc(JA)?(la=!0,kl(xA)):la=!1,xA.memoizedState=Aa.state!==null&&Aa.state!==void 0?Aa.state:null,pm(xA),Aa.updater=Im,xA.stateNode=Aa,Aa._reactInternals=xA,u0(xA,JA,yA,NA),xA=h0(null,xA,JA,!0,la,NA)):(xA.tag=0,$l&&la&&Xp(xA),_c(null,xA,Aa,NA),xA=xA.child),xA;case 16:JA=xA.elementType;A:{switch(Ih(yA,xA),yA=xA.pendingProps,Aa=JA._init,JA=Aa(JA._payload),xA.type=JA,Aa=xA.tag=Xw(JA),yA=Qh(JA,yA),Aa){case 0:xA=RE(null,xA,JA,yA,NA);break A;case 1:xA=bv(null,xA,JA,yA,NA);break A;case 11:xA=Uf(null,xA,JA,yA,NA);break A;case 14:xA=yd(null,xA,JA,Qh(JA.type,yA),NA);break A}throw Error(tA(306,JA,""))}return xA;case 0:return JA=xA.type,Aa=xA.pendingProps,Aa=xA.elementType===JA?Aa:Qh(JA,Aa),RE(yA,xA,JA,Aa,NA);case 1:return JA=xA.type,Aa=xA.pendingProps,Aa=xA.elementType===JA?Aa:Qh(JA,Aa),bv(yA,xA,JA,Aa,NA);case 3:A:{if(fB(xA),yA===null)throw Error(tA(387));JA=xA.pendingProps,la=xA.memoizedState,Aa=la.element,_E(yA,xA),np(xA,JA,null,NA);var Da=xA.memoizedState;if(JA=Da.element,la.isDehydrated)if(la={element:JA,isDehydrated:!1,cache:Da.cache,pendingSuspenseBoundaries:Da.pendingSuspenseBoundaries,transitions:Da.transitions},xA.updateQueue.baseState=la,xA.memoizedState=la,xA.flags&256){Aa=wd(Error(tA(423)),xA),xA=hg(yA,xA,JA,NA,Aa);break A}else if(JA!==Aa){Aa=wd(Error(tA(424)),xA),xA=hg(yA,xA,JA,NA,Aa);break A}else for(Rf=og(xA.stateNode.containerInfo.firstChild),$c=xA,$l=!0,ph=null,NA=nu(xA,null,JA,NA),xA.child=NA;NA;)NA.flags=NA.flags&-3|4096,NA=NA.sibling;else{if(Bh(),JA===Aa){xA=Lf(yA,xA,NA);break A}_c(yA,xA,JA,NA)}xA=xA.child}return xA;case 5:return Iv(xA),yA===null&&Tf(xA),JA=xA.type,Aa=xA.pendingProps,la=yA!==null?yA.memoizedProps:null,Da=Aa.children,jp(JA,Aa)?Da=null:la!==null&&jp(JA,la)&&(xA.flags|=32),Dm(yA,xA),_c(yA,xA,Da,NA),xA.child;case 6:return yA===null&&Tf(xA),null;case 13:return gB(yA,xA,NA);case 4:return Qv(xA,xA.stateNode.containerInfo),JA=xA.pendingProps,yA===null?xA.child=rl(xA,null,JA,NA):_c(yA,xA,JA,NA),xA.child;case 11:return JA=xA.type,Aa=xA.pendingProps,Aa=xA.elementType===JA?Aa:Qh(JA,Aa),Uf(yA,xA,JA,Aa,NA);case 7:return _c(yA,xA,xA.pendingProps,NA),xA.child;case 8:return _c(yA,xA,xA.pendingProps.children,NA),xA.child;case 12:return _c(yA,xA,xA.pendingProps.children,NA),xA.child;case 10:A:{if(JA=xA.type._context,Aa=xA.pendingProps,la=xA.memoizedProps,Da=Aa.value,Cu(Gf,JA._currentValue),JA._currentValue=Da,la!==null)if(Is(la.value,Da)){if(la.children===Aa.children&&!xc.current){xA=Lf(yA,xA,NA);break A}}else for(la=xA.child,la!==null&&(la.return=xA);la!==null;){var Wa=la.dependencies;if(Wa!==null){Da=la.child;for(var rs=Wa.firstContext;rs!==null;){if(rs.context===JA){if(la.tag===1){rs=Ug(-1,NA&-NA),rs.tag=2;var Ds=la.updateQueue;if(Ds!==null){Ds=Ds.shared;var zs=Ds.pending;zs===null?rs.next=rs:(rs.next=zs.next,zs.next=rs),Ds.pending=rs}}la.lanes|=NA,rs=la.alternate,rs!==null&&(rs.lanes|=NA),Wc(la.return,NA,xA),Wa.lanes|=NA;break}rs=rs.next}}else if(la.tag===10)Da=la.type===xA.type?null:la.child;else if(la.tag===18){if(Da=la.return,Da===null)throw Error(tA(341));Da.lanes|=NA,Wa=Da.alternate,Wa!==null&&(Wa.lanes|=NA),Wc(Da,NA,xA),Da=la.sibling}else Da=la.child;if(Da!==null)Da.return=la;else for(Da=la;Da!==null;){if(Da===xA){Da=null;break}if(la=Da.sibling,la!==null){la.return=Da.return,Da=la;break}Da=Da.return}la=Da}_c(yA,xA,Aa.children,NA),xA=xA.child}return xA;case 9:return Aa=xA.type,JA=xA.pendingProps.children,Wl(xA,NA),Aa=mh(Aa),JA=JA(Aa),xA.flags|=1,_c(yA,xA,JA,NA),xA.child;case 14:return JA=xA.type,Aa=Qh(JA,xA.pendingProps),Aa=Qh(JA.type,Aa),yd(yA,xA,JA,Aa,NA);case 15:return Yu(yA,xA,xA.type,xA.pendingProps,NA);case 17:return JA=xA.type,Aa=xA.pendingProps,Aa=xA.elementType===JA?Aa:Qh(JA,Aa),Ih(yA,xA),xA.tag=1,wc(JA)?(yA=!0,kl(xA)):yA=!1,Wl(xA,NA),Og(xA,JA,Aa),u0(xA,JA,Aa,NA),h0(null,xA,JA,!0,yA,NA);case 19:return af(yA,xA,NA);case 22:return cB(yA,xA,NA)}throw Error(tA(156,xA.tag))};function KE(yA,xA){return xa(yA,xA)}function qw(yA,xA,NA,JA){this.tag=yA,this.key=NA,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=xA,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=JA,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Mh(yA,xA,NA,JA){return new qw(yA,xA,NA,JA)}function Gm(yA){return yA=yA.prototype,!(!yA||!yA.isReactComponent)}function Xw(yA){if(typeof yA=="function")return Gm(yA)?1:0;if(yA!=null){if(yA=yA.$$typeof,yA===sA)return 11;if(yA===wA)return 14}return 2}function $h(yA,xA){var NA=yA.alternate;return NA===null?(NA=Mh(yA.tag,xA,yA.key,yA.mode),NA.elementType=yA.elementType,NA.type=yA.type,NA.stateNode=yA.stateNode,NA.alternate=yA,yA.alternate=NA):(NA.pendingProps=xA,NA.type=yA.type,NA.flags=0,NA.subtreeFlags=0,NA.deletions=null),NA.flags=yA.flags&14680064,NA.childLanes=yA.childLanes,NA.lanes=yA.lanes,NA.child=yA.child,NA.memoizedProps=yA.memoizedProps,NA.memoizedState=yA.memoizedState,NA.updateQueue=yA.updateQueue,xA=yA.dependencies,NA.dependencies=xA===null?null:{lanes:xA.lanes,firstContext:xA.firstContext},NA.sibling=yA.sibling,NA.index=yA.index,NA.ref=yA.ref,NA}function I0(yA,xA,NA,JA,Aa,la){var Da=2;if(JA=yA,typeof yA=="function")Gm(yA)&&(Da=1);else if(typeof yA=="string")Da=5;else A:switch(yA){case VA:return Kg(NA.children,Aa,la,xA);case Qi:Da=8,Aa|=8;break;case jA:return yA=Mh(12,NA,xA,Aa|2),yA.elementType=jA,yA.lanes=la,yA;case gA:return yA=Mh(13,NA,xA,Aa),yA.elementType=gA,yA.lanes=la,yA;case BA:return yA=Mh(19,NA,xA,Aa),yA.elementType=BA,yA.lanes=la,yA;case _A:return Dd(NA,Aa,la,xA);default:if(typeof yA=="object"&&yA!==null)switch(yA.$$typeof){case WA:Da=10;break A;case PA:Da=9;break A;case sA:Da=11;break A;case wA:Da=14;break A;case MA:Da=16,JA=null;break A}throw Error(tA(130,yA==null?yA:typeof yA,""))}return xA=Mh(Da,NA,xA,Aa),xA.elementType=yA,xA.type=JA,xA.lanes=la,xA}function Kg(yA,xA,NA,JA){return yA=Mh(7,yA,JA,xA),yA.lanes=NA,yA}function Dd(yA,xA,NA,JA){return yA=Mh(22,yA,JA,xA),yA.elementType=_A,yA.lanes=NA,yA.stateNode={isHidden:!1},yA}function Uv(yA,xA,NA){return yA=Mh(6,yA,null,xA),yA.lanes=NA,yA}function Um(yA,xA,NA){return xA=Mh(4,yA.children!==null?yA.children:[],yA.key,xA),xA.lanes=NA,xA.stateNode={containerInfo:yA.containerInfo,pendingChildren:null,implementation:yA.implementation},xA}function VE(yA,xA,NA,JA,Aa){this.tag=xA,this.containerInfo=yA,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=cc(0),this.expirationTimes=cc(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=cc(0),this.identifierPrefix=JA,this.onRecoverableError=Aa,this.mutableSourceEagerHydrationData=null}function Lm(yA,xA,NA,JA,Aa,la,Da,Wa,rs){return yA=new VE(yA,xA,NA,Wa,rs),xA===1?(xA=1,la===!0&&(xA|=8)):xA=0,la=Mh(3,null,null,xA),yA.current=la,la.stateNode=yA,la.memoizedState={element:JA,isDehydrated:NA,cache:null,transitions:null,pendingSuspenseBoundaries:null},pm(la),yA}function Zw(yA,xA,NA){var JA=31?pA-1:0),LA=1;LA1?pA-1:0),LA=1;LA2&&(fA[0]==="o"||fA[0]==="O")&&(fA[1]==="n"||fA[1]==="N")}function Pa(fA,pA,SA,LA){if(SA!==null&&SA.type===ja)return!1;switch(typeof pA){case"function":case"symbol":return!0;case"boolean":{if(LA)return!1;if(SA!==null)return!SA.acceptsBooleans;var zA=fA.toLowerCase().slice(0,5);return zA!=="data-"&&zA!=="aria-"}default:return!1}}function La(fA,pA,SA,LA){if(pA===null||typeof pA>"u"||Pa(fA,pA,SA,LA))return!0;if(LA)return!1;if(SA!==null)switch(SA.type){case ba:return!pA;case ts:return pA===!1;case qA:return isNaN(pA);case Zi:return isNaN(pA)||pA<1}return!1}function Qs(fA){return za.hasOwnProperty(fA)?za[fA]:null}function Qa(fA,pA,SA,LA,zA,ZA,ra){this.acceptsBooleans=pA===sa||pA===ba||pA===ts,this.attributeName=LA,this.attributeNamespace=zA,this.mustUseProperty=SA,this.propertyName=fA,this.type=pA,this.sanitizeURL=ZA,this.removeEmptyString=ra}var za={},Bs=["children","dangerouslySetInnerHTML","defaultValue","defaultChecked","innerHTML","suppressContentEditableWarning","suppressHydrationWarning","style"];Bs.forEach(function(fA){za[fA]=new Qa(fA,ja,!1,fA,null,!1,!1)}),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(fA){var pA=fA[0],SA=fA[1];za[pA]=new Qa(pA,ya,!1,SA,null,!1,!1)}),["contentEditable","draggable","spellCheck","value"].forEach(function(fA){za[fA]=new Qa(fA,sa,!1,fA.toLowerCase(),null,!1,!1)}),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(fA){za[fA]=new Qa(fA,sa,!1,fA,null,!1,!1)}),["allowFullScreen","async","autoFocus","autoPlay","controls","default","defer","disabled","disablePictureInPicture","disableRemotePlayback","formNoValidate","hidden","loop","noModule","noValidate","open","playsInline","readOnly","required","reversed","scoped","seamless","itemScope"].forEach(function(fA){za[fA]=new Qa(fA,ba,!1,fA.toLowerCase(),null,!1,!1)}),["checked","multiple","muted","selected"].forEach(function(fA){za[fA]=new Qa(fA,ba,!0,fA,null,!1,!1)}),["capture","download"].forEach(function(fA){za[fA]=new Qa(fA,ts,!1,fA,null,!1,!1)}),["cols","rows","size","span"].forEach(function(fA){za[fA]=new Qa(fA,Zi,!1,fA,null,!1,!1)}),["rowSpan","start"].forEach(function(fA){za[fA]=new Qa(fA,qA,!1,fA.toLowerCase(),null,!1,!1)});var Xa=/[\-\:]([a-z])/g,Ka=function(fA){return fA[1].toUpperCase()};["accent-height","alignment-baseline","arabic-form","baseline-shift","cap-height","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","dominant-baseline","enable-background","fill-opacity","fill-rule","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","horiz-adv-x","horiz-origin-x","image-rendering","letter-spacing","lighting-color","marker-end","marker-mid","marker-start","overline-position","overline-thickness","paint-order","panose-1","pointer-events","rendering-intent","shape-rendering","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-anchor","text-decoration","text-rendering","underline-position","underline-thickness","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","vector-effect","vert-adv-y","vert-origin-x","vert-origin-y","word-spacing","writing-mode","xmlns:xlink","x-height"].forEach(function(fA){var pA=fA.replace(Xa,Ka);za[pA]=new Qa(pA,ya,!1,fA,null,!1,!1)}),["xlink:actuate","xlink:arcrole","xlink:role","xlink:show","xlink:title","xlink:type"].forEach(function(fA){var pA=fA.replace(Xa,Ka);za[pA]=new Qa(pA,ya,!1,fA,"http://www.w3.org/1999/xlink",!1,!1)}),["xml:base","xml:lang","xml:space"].forEach(function(fA){var pA=fA.replace(Xa,Ka);za[pA]=new Qa(pA,ya,!1,fA,"http://www.w3.org/XML/1998/namespace",!1,!1)}),["tabIndex","crossOrigin"].forEach(function(fA){za[fA]=new Qa(fA,ya,!1,fA.toLowerCase(),null,!1,!1)});var vs="xlinkHref";za[vs]=new Qa("xlinkHref",ya,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach(function(fA){za[fA]=new Qa(fA,ya,!1,fA.toLowerCase(),null,!0,!0)});var As=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i,ls=!1;function tu(fA){!ls&&As.test(fA)&&(ls=!0,oA("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML try using dangerouslySetInnerHTML instead. React was passed %s.",JSON.stringify(fA)))}function ha(fA,pA,SA,LA){if(LA.mustUseProperty){var zA=LA.propertyName;return fA[zA]}else{Ia(SA,pA),LA.sanitizeURL&&tu(""+SA);var ZA=LA.attributeName,ra=null;if(LA.type===ts){if(fA.hasAttribute(ZA)){var ga=fA.getAttribute(ZA);return ga===""?!0:La(pA,SA,LA,!1)?ga:ga===""+SA?SA:ga}}else if(fA.hasAttribute(ZA)){if(La(pA,SA,LA,!1))return fA.getAttribute(ZA);if(LA.type===ba)return SA;ra=fA.getAttribute(ZA)}return La(pA,SA,LA,!1)?ra===null?SA:ra:ra===""+SA?SA:ra}}function ea(fA,pA,SA,LA){{if(!pa(pA))return;if(!fA.hasAttribute(pA))return SA===void 0?void 0:null;var zA=fA.getAttribute(pA);return Ia(SA,pA),zA===""+SA?SA:zA}}function fa(fA,pA,SA,LA){var zA=Qs(pA);if(!Ha(pA,zA,LA)){if(La(pA,SA,zA,LA)&&(SA=null),LA||zA===null){if(pa(pA)){var ZA=pA;SA===null?fA.removeAttribute(ZA):(Ia(SA,pA),fA.setAttribute(ZA,""+SA))}return}var ra=zA.mustUseProperty;if(ra){var ga=zA.propertyName;if(SA===null){var Ba=zA.type;fA[ga]=Ba===ba?!1:""}else fA[ga]=SA;return}var Ya=zA.attributeName,Ua=zA.attributeNamespace;if(SA===null)fA.removeAttribute(Ya);else{var us=zA.type,ss;us===ba||us===ts&&SA===!0?ss="":(Ia(SA,Ya),ss=""+SA,zA.sanitizeURL&&tu(ss.toString())),Ua?fA.setAttributeNS(Ua,Ya,ss):fA.setAttribute(Ya,ss)}}}var ma=Symbol.for("react.element"),Fa=Symbol.for("react.portal"),os=Symbol.for("react.fragment"),Oa=Symbol.for("react.strict_mode"),hs=Symbol.for("react.profiler"),ru=Symbol.for("react.provider"),Ca=Symbol.for("react.context"),$a=Symbol.for("react.forward_ref"),xs=Symbol.for("react.suspense"),es=Symbol.for("react.suspense_list"),ps=Symbol.for("react.memo"),Js=Symbol.for("react.lazy"),Xs=Symbol.for("react.scope"),lu=Symbol.for("react.debug_trace_mode"),Fu=Symbol.for("react.offscreen"),Mu=Symbol.for("react.legacy_hidden"),xa=Symbol.for("react.cache"),Za=Symbol.for("react.tracing_marker"),fs=Symbol.iterator,ds="@@iterator";function Ja(fA){if(fA===null||typeof fA!="object")return null;var pA=fs&&fA[fs]||fA[ds];return typeof pA=="function"?pA:null}var Es=Object.assign,Ls=0,Ws,Os,qs,Zs,Vs,Au,vu;function Ju(){}Ju.__reactDisabledLog=!0;function al(){{if(Ls===0){Ws=console.log,Os=console.info,qs=console.warn,Zs=console.error,Vs=console.group,Au=console.groupCollapsed,vu=console.groupEnd;var fA={configurable:!0,enumerable:!0,value:Ju,writable:!0};Object.defineProperties(console,{info:fA,log:fA,warn:fA,error:fA,group:fA,groupCollapsed:fA,groupEnd:fA})}Ls++}}function Bl(){{if(Ls--,Ls===0){var fA={configurable:!0,enumerable:!0,writable:!0};Object.defineProperties(console,{log:Es({},fA,{value:Ws}),info:Es({},fA,{value:Os}),warn:Es({},fA,{value:qs}),error:Es({},fA,{value:Zs}),group:Es({},fA,{value:Vs}),groupCollapsed:Es({},fA,{value:Au}),groupEnd:Es({},fA,{value:vu})})}Ls<0&&oA("disabledDepth fell below zero. This is a bug in React. Please file an issue.")}}var wu=tA.ReactCurrentDispatcher,gl;function ku(fA,pA,SA){{if(gl===void 0)try{throw Error()}catch(zA){var LA=zA.stack.trim().match(/\n( *(at )?)/);gl=LA&&LA[1]||""}return` +`+gl+fA}}var Ul=!1,uc;{var lc=typeof WeakMap=="function"?WeakMap:Map;uc=new lc}function zf(fA,pA){if(!fA||Ul)return"";{var SA=uc.get(fA);if(SA!==void 0)return SA}var LA;Ul=!0;var zA=Error.prepareStackTrace;Error.prepareStackTrace=void 0;var ZA;ZA=wu.current,wu.current=null,al();try{if(pA){var ra=function(){throw Error()};if(Object.defineProperty(ra.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(ra,[])}catch(Ps){LA=Ps}Reflect.construct(fA,[],ra)}else{try{ra.call()}catch(Ps){LA=Ps}fA.call(ra.prototype)}}else{try{throw Error()}catch(Ps){LA=Ps}fA()}}catch(Ps){if(Ps&&LA&&typeof Ps.stack=="string"){for(var ga=Ps.stack.split(` +`),Ba=LA.stack.split(` +`),Ya=ga.length-1,Ua=Ba.length-1;Ya>=1&&Ua>=0&&ga[Ya]!==Ba[Ua];)Ua--;for(;Ya>=1&&Ua>=0;Ya--,Ua--)if(ga[Ya]!==Ba[Ua]){if(Ya!==1||Ua!==1)do if(Ya--,Ua--,Ua<0||ga[Ya]!==Ba[Ua]){var us=` +`+ga[Ya].replace(" at new "," at ");return fA.displayName&&us.includes("")&&(us=us.replace("",fA.displayName)),typeof fA=="function"&&uc.set(fA,us),us}while(Ya>=1&&Ua>=0);break}}}finally{Ul=!1,wu.current=ZA,Bl(),Error.prepareStackTrace=zA}var ss=fA?fA.displayName||fA.name:"",bs=ss?ku(ss):"";return typeof fA=="function"&&uc.set(fA,bs),bs}function ml(fA,pA,SA){return zf(fA,!0)}function mc(fA,pA,SA){return zf(fA,!1)}function cc(fA){var pA=fA.prototype;return!!(pA&&pA.isReactComponent)}function zl(fA,pA,SA){if(fA==null)return"";if(typeof fA=="function")return zf(fA,cc(fA));if(typeof fA=="string")return ku(fA);switch(fA){case xs:return ku("Suspense");case es:return ku("SuspenseList")}if(typeof fA=="object")switch(fA.$$typeof){case $a:return mc(fA.render);case ps:return zl(fA.type,pA,SA);case Js:{var LA=fA,zA=LA._payload,ZA=LA._init;try{return zl(ZA(zA),pA,SA)}catch{}}}return""}function Dc(fA){switch(fA._debugOwner&&fA._debugOwner.type,fA._debugSource,fA.tag){case vA:return ku(fA.type);case Qi:return ku("Lazy");case UA:return ku("Suspense");case PA:return ku("SuspenseList");case uA:case cA:case VA:return mc(fA.type);case GA:return mc(fA.type.render);case lA:return ml(fA.type);default:return""}}function ff(fA){try{var pA="",SA=fA;do pA+=Dc(SA),SA=SA.return;while(SA);return pA}catch(LA){return` +Error generating stack: `+LA.message+` +`+LA.stack}}function Ku(fA,pA,SA){var LA=fA.displayName;if(LA)return LA;var zA=pA.displayName||pA.name||"";return zA!==""?SA+"("+zA+")":SA}function Fh(fA){return fA.displayName||"Context"}function sl(fA){if(fA==null)return null;if(typeof fA.tag=="number"&&oA("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),typeof fA=="function")return fA.displayName||fA.name||null;if(typeof fA=="string")return fA;switch(fA){case os:return"Fragment";case Fa:return"Portal";case hs:return"Profiler";case Oa:return"StrictMode";case xs:return"Suspense";case es:return"SuspenseList"}if(typeof fA=="object")switch(fA.$$typeof){case Ca:var pA=fA;return Fh(pA)+".Consumer";case ru:var SA=fA;return Fh(SA._context)+".Provider";case $a:return Ku(fA,fA.render,"ForwardRef");case ps:var LA=fA.displayName||null;return LA!==null?LA:sl(fA.type)||"Memo";case Js:{var zA=fA,ZA=zA._payload,ra=zA._init;try{return sl(ra(ZA))}catch{return null}}}return null}function Sp(fA,pA,SA){var LA=pA.displayName||pA.name||"";return fA.displayName||(LA!==""?SA+"("+LA+")":SA)}function Zh(fA){return fA.displayName||"Context"}function Lu(fA){var pA=fA.tag,SA=fA.type;switch(pA){case wA:return"Cache";case QA:var LA=SA;return Zh(LA)+".Consumer";case FA:var zA=SA;return Zh(zA._context)+".Provider";case WA:return"DehydratedFragment";case GA:return Sp(SA,SA.render,"ForwardRef");case CA:return"Fragment";case vA:return SA;case mA:return"Portal";case dA:return"Root";case EA:return"Text";case Qi:return sl(SA);case IA:return SA===Oa?"StrictMode":"Mode";case gA:return"Offscreen";case YA:return"Profiler";case sA:return"Scope";case UA:return"Suspense";case PA:return"SuspenseList";case MA:return"TracingMarker";case lA:case uA:case jA:case cA:case OA:case VA:if(typeof SA=="function")return SA.displayName||SA.name||null;if(typeof SA=="string")return SA;break}return null}var Hd=tA.ReactDebugCurrentFrame,Gc=null,Yh=!1;function hf(){{if(Gc===null)return null;var fA=Gc._debugOwner;if(fA!==null&&typeof fA<"u")return Lu(fA)}return null}function Ag(){return Gc===null?"":ff(Gc)}function jl(){Hd.getCurrentStack=null,Gc=null,Yh=!1}function _l(fA){Hd.getCurrentStack=fA===null?null:Ag,Gc=fA,Yh=!1}function Ad(){return Gc}function bc(fA){Yh=fA}function gf(fA){return""+fA}function sh(fA){switch(typeof fA){case"boolean":case"number":case"string":case"undefined":return fA;case"object":return Na(fA),fA;default:return""}}var yg={button:!0,checkbox:!0,image:!0,hidden:!0,radio:!0,reset:!0,submit:!0};function HB(fA,pA){yg[pA.type]||pA.onChange||pA.onInput||pA.readOnly||pA.disabled||pA.value==null||oA("You provided a `value` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultValue`. Otherwise, set either `onChange` or `readOnly`."),pA.onChange||pA.readOnly||pA.disabled||pA.checked==null||oA("You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly`.")}function kB(fA){var pA=fA.type,SA=fA.nodeName;return SA&&SA.toLowerCase()==="input"&&(pA==="checkbox"||pA==="radio")}function ed(fA){return fA._valueTracker}function kd(fA){fA._valueTracker=null}function dv(fA){var pA="";return fA&&(kB(fA)?pA=fA.checked?"true":"false":pA=fA.value),pA}function nl(fA){var pA=kB(fA)?"checked":"value",SA=Object.getOwnPropertyDescriptor(fA.constructor.prototype,pA);Na(fA[pA]);var LA=""+fA[pA];if(!(fA.hasOwnProperty(pA)||typeof SA>"u"||typeof SA.get!="function"||typeof SA.set!="function")){var zA=SA.get,ZA=SA.set;Object.defineProperty(fA,pA,{configurable:!0,get:function(){return zA.call(this)},set:function(ga){Na(ga),LA=""+ga,ZA.call(this,ga)}}),Object.defineProperty(fA,pA,{enumerable:SA.enumerable});var ra={getValue:function(){return LA},setValue:function(ga){Na(ga),LA=""+ga},stopTracking:function(){kd(fA),delete fA[pA]}};return ra}}function il(fA){ed(fA)||(fA._valueTracker=nl(fA))}function eg(fA){if(!fA)return!1;var pA=ed(fA);if(!pA)return!0;var SA=pA.getValue(),LA=dv(fA);return LA!==SA?(pA.setValue(LA),!0):!1}function uh(fA){if(fA=fA||(typeof document<"u"?document:void 0),typeof fA>"u")return null;try{return fA.activeElement||fA.body}catch{return fA.body}}var _p=!1,Fp=!1,td=!1,Nd=!1;function Od(fA){var pA=fA.type==="checkbox"||fA.type==="radio";return pA?fA.checked!=null:fA.value!=null}function Yp(fA,pA){var SA=fA,LA=pA.checked,zA=Es({},pA,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:LA??SA._wrapperState.initialChecked});return zA}function Rl(fA,pA){HB("input",pA),pA.checked!==void 0&&pA.defaultChecked!==void 0&&!Fp&&(oA("%s contains an input of type %s with both checked and defaultChecked props. Input elements must be either controlled or uncontrolled (specify either the checked prop, or the defaultChecked prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",hf()||"A component",pA.type),Fp=!0),pA.value!==void 0&&pA.defaultValue!==void 0&&!_p&&(oA("%s contains an input of type %s with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props. More info: https://reactjs.org/link/controlled-components",hf()||"A component",pA.type),_p=!0);var SA=fA,LA=pA.defaultValue==null?"":pA.defaultValue;SA._wrapperState={initialChecked:pA.checked!=null?pA.checked:pA.defaultChecked,initialValue:sh(pA.value!=null?pA.value:LA),controlled:Od(pA)}}function va(fA,pA){var SA=fA,LA=pA.checked;LA!=null&&fa(SA,"checked",LA,!1)}function qa(fA,pA){var SA=fA;{var LA=Od(pA);!SA._wrapperState.controlled&&LA&&!Nd&&(oA("A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),Nd=!0),SA._wrapperState.controlled&&!LA&&!td&&(oA("A component is changing a controlled input to be uncontrolled. This is likely caused by the value changing from a defined to undefined, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components"),td=!0)}va(fA,pA);var zA=sh(pA.value),ZA=pA.type;if(zA!=null)ZA==="number"?(zA===0&&SA.value===""||SA.value!=zA)&&(SA.value=gf(zA)):SA.value!==gf(zA)&&(SA.value=gf(zA));else if(ZA==="submit"||ZA==="reset"){SA.removeAttribute("value");return}pA.hasOwnProperty("value")?Du(SA,pA.type,zA):pA.hasOwnProperty("defaultValue")&&Du(SA,pA.type,sh(pA.defaultValue)),pA.checked==null&&pA.defaultChecked!=null&&(SA.defaultChecked=!!pA.defaultChecked)}function Ms(fA,pA,SA){var LA=fA;if(pA.hasOwnProperty("value")||pA.hasOwnProperty("defaultValue")){var zA=pA.type,ZA=zA==="submit"||zA==="reset";if(ZA&&(pA.value===void 0||pA.value===null))return;var ra=gf(LA._wrapperState.initialValue);SA||ra!==LA.value&&(LA.value=ra),LA.defaultValue=ra}var ga=LA.name;ga!==""&&(LA.name=""),LA.defaultChecked=!LA.defaultChecked,LA.defaultChecked=!!LA._wrapperState.initialChecked,ga!==""&&(LA.name=ga)}function _s(fA,pA){var SA=fA;qa(SA,pA),js(SA,pA)}function js(fA,pA){var SA=pA.name;if(pA.type==="radio"&&SA!=null){for(var LA=fA;LA.parentNode;)LA=LA.parentNode;Ia(SA,"name");for(var zA=LA.querySelectorAll("input[name="+JSON.stringify(""+SA)+'][type="radio"]'),ZA=0;ZA.")))}):pA.dangerouslySetInnerHTML!=null&&(Xu||(Xu=!0,oA("Pass a `value` prop if you set dangerouslyInnerHTML so React knows which value should be selected.")))),pA.selected!=null&&!eu&&(oA("Use the `defaultValue` or `value` props on must be a scalar value if `multiple` is false.%s",SA,Xl())}}}}function ch(fA,pA,SA,LA){var zA=fA.options;if(pA){for(var ZA=SA,ra={},ga=0;ga.");var LA=Es({},pA,{value:void 0,defaultValue:void 0,children:gf(SA._wrapperState.initialValue)});return LA}function mv(fA,pA){var SA=fA;HB("textarea",pA),pA.value!==void 0&&pA.defaultValue!==void 0&&!K0&&(oA("%s contains a textarea with both value and defaultValue props. Textarea elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled textarea and remove one of these props. More info: https://reactjs.org/link/controlled-components",hf()||"A component"),K0=!0);var LA=pA.value;if(LA==null){var zA=pA.children,ZA=pA.defaultValue;if(zA!=null){oA("Use the `defaultValue` or `value` props instead of setting children on