diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..619163b --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# TezOracle runtime placeholders. +# Copy to `.env` for local/testnet use. Never commit `.env` or production values. +# This repository is not authorized for production keys, endpoints, or signer material. + +# Tezos RPC used for simulation and (later) testnet origination. +TEZOS_RPC_URL= +TEZOS_NETWORK=ghostnet + +# Originated oracle contract and chain. Leave empty until a testnet deploy exists. +ORACLE_ADDRESS= +TEZOS_CHAIN_ID= + +# Testnet-only signer secret (e.g. edsk...). Must never be a production key. +TEZORACLE_SIGNER_SECRET_KEY= + +# Optional local services. Coordinator and relayer hold no signing keys. +COORDINATOR_URL= +RELAYER_RPC_URL= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..eddb2c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI + +on: + push: + branches: [main, master, dev] + pull_request: + +permissions: + contents: read + +# Skeleton checks only. Packing-vector parity (byte-for-byte SmartPy vs TypeScript) +# is added after the payload freeze. +# This workflow uses no GitHub secrets, production credentials, or signer keys. + +jobs: + typescript: + name: TypeScript + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: .nvmrc + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm test + + python: + name: SmartPy / Python + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version-file: .python-version + - run: python -m pip install -r requirements-dev.txt + - run: python -m pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0adc6f --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +# Secrets and local credentials — never commit these +.env +.env.local +.env.*.local +*.pem +*.key +*.p12 +*.pfx +id_rsa +id_rsa.pub +id_ed25519 +id_ed25519.pub +*.secret +secrets/ +wallets/ +*.wallet +faucet.json + +# Node.js +node_modules/ +dist/ +build/ +coverage/ +.nyc_output/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Python +__pycache__/ +*.py[cod] +*$py.class +.Python +.venv/ +venv/ +ENV/ +*.egg-info/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.coverage +htmlcov/ +.tox/ + +# SmartPy / local compile dumps +.smartpy/ +smartpy_output/ +.sp_build/ +**/__pycache__/ + +# OS and editor +.DS_Store +Thumbs.db +.idea/ +*.swp +*.swo +*~ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b9e03d --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, or + MERCHANTABILITY. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Stable Technologies + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 987ddb5..c346c54 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# tezoracle \ No newline at end of file +# TezOracle + +Configurable N-of-M Tezos Layer 1 price oracle. Generic oracle contracts, validators, coordinator, relayer, tests, and oracle docs live in this repository. TezFin consumer logic (aliases, normalization, max age, bounds, `getValidatedPrice`) stays in `StableTechnologies/TezFin`. + +## Status: not production + +**This repository is not a production price feed.** The authorized initial phase is **testnet and non-authoritative shadow only**. It does not authorize: + +- TezFin production reliance or `set_oracle` +- production 3-of-4 cross-class quorum +- Rust Class B, or four isolated production signer environments +- production credentials, keys, or endpoints + +A 1-of-1 signer configuration is permitted only for testnet and non-authoritative shadow. Pause is immediate; unpause and every risk-increasing change are delayed. USDtz and tzBTC remain non-authoritative until separately reviewed. + +## What this project is + +TezOracle publishes signed prices on Tezos using a **configurable N-of-M** Michelson contract. One compiled artifact supports many thresholds (including 1-of-1 for testnet/shadow and 3-of-4 as a configuration, never as hard-coded contract logic). Class A validators observe approved CEX sources, derive prices under a pinned policy, and sign a frozen payload. A non-authoritative coordinator may trigger a round and present a candidate. A permissionless relayer submits already-signed bytes and holds no keys. + +The upstream view returns `(nat price, timestamp observation_time)`. Observation time is the market time used to derive the price, not Tezos inclusion time. + +```text +Approved CEX sources + | + v +Class A (TypeScript) + | + v +Coordinator (candidate only; no keys) + | + v +Permissionless relayer (cannot mutate signed bytes) + | + v +N-of-M Michelson oracle + | + v +TezFinOracle wrapper (in TezFin) +``` + +This project does not depend on Acurast, Acelon, or Ubinetic. + +## Quick start + +Requirements: Node.js 22 (see `.nvmrc`) and Python 3.12 (see `.python-version`). Skeleton tests do not need credentials. + +```bash +git clone https://github.com/StableTechnologies/TezOracle.git +cd TezOracle + +npm ci +npm run typecheck +npm test + +python3 -m venv .venv +source .venv/bin/activate +python -m pip install -r requirements-dev.txt +python -m pytest +``` + +`.env.example` is placeholders only. Copy it to `.env` only for later local/testnet runs. Never add production secrets. + +The current tree is a public baseline: layout, docs, and CI skeleton. Payload freeze, packing parity, the N-of-M contract, Class A adapters, coordinator/relayer, and local e2e follow in later PRs. Signing is blocked until packing golden vectors pass. + +## Repository layout + +| Path | Role | +| --- | --- | +| `src/contract/` | SmartPy N-of-M contract | +| `src/validator/` | Class A TypeScript validator | +| `src/packing/` | Canonical payload packing | +| `src/coordinator/` | Non-authoritative round coordinator | +| `src/relayer/` | Permissionless submission | +| `tests/` | Contract, validator, packing, and later e2e tests | +| `config/` | Versioned asset parameter register | +| `docs/` | Architecture, security, specs, engineering response | +| `.github/workflows/` | TypeScript and Python CI (no secrets) | + +## Documentation + +| Document | Contents | +| --- | --- | +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | N-of-M, Class A, future Class B, coordinator, relayer, TezFin boundary | +| [docs/SECURITY.md](docs/SECURITY.md) | Threat model, failure modes, keys, replay, testnet limits | +| [docs/ROADMAP.md](docs/ROADMAP.md) | Initial authorized scope vs separately approved production work | +| [docs/ENGINEERING_RESPONSE.md](docs/ENGINEERING_RESPONSE.md) | Design response to the TezFin oracle security specification | + +Payload, evidence, observer-agreement, parameter-schema, contract, Class A, coordinator, relayer, and oracle-interface specs are added when those workstreams start. Until then, treat [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) as the controlling overview. + +## License + +Apache License 2.0. See [LICENSE](LICENSE). diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..f739685 --- /dev/null +++ b/config/README.md @@ -0,0 +1,7 @@ +# Asset and policy configuration + +This directory holds the version-controlled parameter register. + +No production, shadow, or testnet asset policy is committed in the repository +skeleton. Draft, testnet, shadow, and non-authoritative stub configs are added +with the payload and evidence freeze. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..eb95117 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,137 @@ +# Architecture + +TezOracle is a generic Tezos Layer 1 price oracle. This repository owns the Michelson contract, off-chain validators, coordinator, relayer, tests, and oracle documentation. TezFin-specific consumer behavior stays in `StableTechnologies/TezFin`. + +**Current authorization:** testnet and non-authoritative shadow. This document describes the intended architecture, including production-phase components that are **not** being built in the initial phase. + +## Design constraints + +- Configurable **N-of-M from the start**. Never a disposable `SimplePriceOracle`. Never hard-code 3-of-4. 1-of-1 is testnet/shadow only. +- Coordinator is non-authoritative; relayer is permissionless and holds no signing keys. +- Pause is immediate; unpause and all risk-increasing changes are delayed. +- Upstream view returns `(nat price, timestamp observation_time)`. Do not duplicate TezFin `configureMaxPriceAge` or `configurePriceBounds`. +- No Acurast, Acelon, or Ubinetic. +- No production credentials, keys, or endpoints in this repository. +- USDtz and tzBTC stay non-authoritative until separately reviewed. + +## Data path (initial phase) + +```text +Approved CEX sources + | + v +Class A TypeScript validator + observe -> derive under pinned policy -> verify candidate -> sign frozen payload + | + v +Coordinator (optional candidate; no keys; does not choose price or policy) + | + v +Permissionless relayer (local verify, simulate, broadcast; cannot mutate signed bytes) + | + v +N-of-M Michelson oracle + | + v +TezFinOracle wrapper (TezFin repo) + | + v +Comptroller consumer safeguards (TezFin repo) +``` + +Anyone may submit a valid signed batch. A backup relayer can relay the same bytes without access to signing keys. + +## On-chain: configurable N-of-M contract + +One SmartPy contract, compiled to Michelson, is parameterized by: + +| Parameter | Meaning | +| --- | --- | +| `M` | Active signer set size | +| `N` | Signature threshold | +| Class minima | Minimum signatures required from each validator class (`0` allowed for 1-of-1) | +| Signer set | Index, public key, and class per signer | + +The contract: + +- Accepts permissionless `submit`. +- Verifies `CHECK_SIGNATURE` over frozen `PACK(payload)`. +- Rejects unknown, duplicate, and inactive signers. +- Domain-separates with `chain_id` and the oracle contract address. +- Enforces a monotonic per-publication-group round. +- Rejects stale and future observation times per policy. +- Distinguishes pending vs active price with a non-zero activation delay. +- Exposes a public view of only the mature `(nat, timestamp)` using **observation** time, not inclusion time. +- Pauses immediately; resumes and governance (signer set, N/M, class minima, policy hash, assets) activate only after delay. + +Publication groups (`CORE`, `USDTZ`, `TZBTC`) isolate asset families so a USDtz or tzBTC failure does not block a healthy core update. + +## Off-chain: Class A (this phase) + +Class A is a TypeScript validator. It: + +- Reads approved CEX adapters (initial phase: two named venues, with remaining adapters as stretch). +- Derives price, oldest contributing observation time, and evidence from the versioned parameter register. +- Independently verifies any coordinator candidate under the pinned policy and **does not sign on mismatch**. +- Signs only the frozen canonical payload, using testnet keys from runtime configuration. + +Policy (sources, min observations, deviation, aggregation, rounding, decimals) is never taken from a request, coordinator, or relayer. + +## Off-chain: Class B (not this phase) + +Production independence requires a second validation class that does not reuse Class A's acceptance function, policy parser, or compiled artifact. The proposed Class B language is Rust. Class B, four isolated signer environments (A1, A2, B1, B2), and production 3-of-4 with class minima are **separately authorized** work. See [ROADMAP.md](ROADMAP.md). + +## Coordinator + +The coordinator may trigger a round and may assemble a candidate payload for validators to consider. It: + +- Holds **no** signing keys. +- Does **not** choose the authoritative price. +- Does **not** supply or override policy (sources, tolerance, decimals, aggregation, freshness). + +Validators independently derive or verify under the pinned, version-controlled policy. + +## Relayer + +The relayer is permissionless: + +- Verifies signatures locally. +- Simulates the contract call. +- Broadcasts accepted operations. +- Must not modify signed payload bytes. +- Holds no signing keys. + +A backup relayer must be able to submit the same signed batch if the primary path is down. + +## TezFin boundary + +| Lives in TezOracle | Lives in TezFin | +| --- | --- | +| N-of-M contract, signer set, rounds, pause/governance | Aliases and symbol compatibility | +| Frozen payload, evidence, observer agreement | Normalization | +| Class A (and later Class B) validation | `configureMaxPriceAge`, `configurePriceBounds` | +| Coordinator and relayer | `getValidatedPrice` and Comptroller checks | +| View: `(nat price, timestamp observation_time)` | Consumer freshness, bounds, and market policy | + +This repository must not add TezFin-specific wrapper or Comptroller code. + +## Payload (frozen in a later PR) + +Logical fields, in specification order: + +`domain`, `chain_id`, `oracle_address`, `config_version`, `policy_hash`, `publication_group`, `round`, `valid_from`, `valid_until`, `evidence_digest`, ordered asset list (`asset_id`, `price`, `decimals`, `observation_time`). + +Proposed domain: `TEZORACLE_V1`. Signatures cover `PACK(payload)` exactly. Packing implementations must not silently reorder or normalize fields. Signing is blocked until SmartPy and TypeScript golden vectors match byte-for-byte. + +## Repository map + +```text +src/contract/ SmartPy N-of-M contract +src/validator/ Class A TypeScript +src/packing/ Canonical PACK / digest +src/coordinator/ Non-authoritative coordinator +src/relayer/ Permissionless relayer +tests/ Unit, contract, packing, e2e +config/ Versioned parameter register +docs/ Specs and design record +``` diff --git a/docs/ENGINEERING_RESPONSE.md b/docs/ENGINEERING_RESPONSE.md new file mode 100644 index 0000000..9bb22df --- /dev/null +++ b/docs/ENGINEERING_RESPONSE.md @@ -0,0 +1,359 @@ +# TezFin Resilient Oracle + +## Engineering Response + +**Date:** 14 August 2026 +**Status:** Design response and implementation baseline; not production approval + +> Archived in `docs/` as the 14 August 2026 design record. The authorized TezOracle implementation phase is **testnet and non-authoritative shadow only**. Controlling repo docs are [ARCHITECTURE.md](ARCHITECTURE.md), [SECURITY.md](SECURITY.md), and [ROADMAP.md](ROADMAP.md). Upstream does **not** duplicate TezFin `configureMaxPriceAge` or `configurePriceBounds`; that boundary is recorded in the later `ORACLE_INTERFACE` spec. + +## 1. Executive response + +The specification is understood and accepted as the production security baseline. The earlier `OracleCexOnlyPlan.md` will be retained as an implementation inventory and cost baseline only. + +The production design will be a permissionless, threshold-signed Michelson oracle with four separately administered signers in two independent validation classes. The initial quorum will be `3-of-4`, with at least one signature from each class. A single publisher is permitted only for testnet and non-authoritative mainnet shadow operation. + +The intended integration is: + +```text +Comptroller -> TezFinOracle -> ThresholdPriceOracle +``` + + +## 2. Baseline and known repository discrepancy + +The existing integration has these relevant constraints: + +- `TezFinOracle.get_price_with_timestamp` calls the upstream view with `(nat price, timestamp)` and returns `(timestamp, nat)` to consumers. +- `Comptroller.updateAssetPricesWithView` consumes `TezFinOracle.getValidatedPrice` and retains consumer-side freshness, bounds, and movement checks. +- `Comptroller` configures `configureMaxPriceAge` and `configurePriceBounds` on its selected oracle. +- Governance changes the Comptroller oracle through `setPriceOracleAndTimeDiff`. +- Existing legacy symbols include forms such as `XTZUSDT`, `USDTUSDT`, `TZBTCUSDT`, and `*-USD` names. + +The new oracle will therefore expose compatibility entrypoints or use a reviewed adapter. It will not rely on symbol-name inference. + +## 3. Requirement response + + +### Accepted requirements + +The following are accepted without a planned security deviation: + +- production `3-of-4` threshold signatures; +- exactly two class-A and two class-B signers at initial activation; +- at least one signer from each validation class in every accepted quorum; +- permissionless submission and replaceable relayers; +- no requester- or coordinator-controlled source policy, price tolerance, decimals, or aggregation rule; +- canonical asset IDs and explicit compatibility aliases; +- observation timestamps distinct from Tezos inclusion timestamps; +- oldest-contributing-observation timestamp for derived prices; +- exact integer/fixed-point arithmetic; +- signed domain, chain ID, oracle address, configuration version, policy hash, group, round, validity window, asset entries, and evidence digest; +- independent core, USDtz, and tzBTC publication groups; +- non-zero activation delay and separate pending/active state; +- per-asset pause and fail-closed behavior; +- delayed, versioned signer, threshold, policy, bounds, alias, decimal, and asset changes; +- two-step administration and multisig final authority; +- external dead-man monitoring plus a second critical alert channel; +- reproducible builds, locked dependencies, artifact hashes, evidence records, and independent human security review; +- at least 30 consecutive days of shadow operation using the final production configuration. + + + +### Deliberate deviations or clarifications + +1. **Asset scope.** The production canonical set is `XTZ_USD`, `BTC_USD`, `USDT_USD`, `USDTZ_USD`, and `TZBTC_USD`. The earlier four-asset CEX plan is insufficient because it does not define the tzBTC peg risk. +2. **Compatibility API.** The threshold contract will provide `get_price_with_timestamp`, `configureMaxPriceAge`, and `configurePriceBounds` compatibility surfaces, or a separately reviewed adapter will provide them. This is required by the current TezFin wrapper and is not an authority shortcut. +3. **Publication atomicity.** Atomicity applies within an approved publication group, not across all assets. A failed USDtz or tzBTC derivation must not stop a healthy core update. +4. **Degraded DEX mode.** No one-pool USDtz degraded mode is enabled in the initial release. It may be added only through a delayed governance policy change and a new configuration version. +5. **tzBTC.** `TZBTC_USD` will remain non-authoritative for collateral and borrowing until a reviewed tzBTC/BTC peg policy and liquidity evidence exist. BTC/USD will not be used as an implicit tzBTC alias. The proposed initial policy is to publish tzBTC only as `BTC_USD * TZBTC_BTC_PEG`, where `TZBTC_BTC_PEG` is derived from approved Tezos liquidity routes using executable quotes, a rolling TWAP, minimum liquidity, bounded price impact, cross-route deviation, and the oldest contributing observation time. If those conditions are not met, the tzBTC group fails closed. No fixed peg, issuer assertion, or coordinator-selected haircut is accepted. +6. **AWS topology.** AWS Lambda/EventBridge is acceptable for each isolated signer domain and for non-authoritative services. A shared coordinator/relayer may not hold signing keys or become a policy authority. + +## 4. Validator-class design + + +### Class A: venue-first market validator + +Class A uses direct exchange adapters for Binance, OKX, Kraken, and Coinbase. It retrieves approved market paths, validates venue identity and timestamps, filters malformed or stale observations, applies the fixed source quorum and deviation policy, and calculates median-based prices using integer arithmetic. USDtz uses the approved Tezos DEX quote/TWAP path and USDT/USD. tzBTC is rejected unless the approved peg-factor route is available. + +For DEX-derived assets, Class A reads the approved pool addresses and raw chain state directly through its configured RPC clients. It may use the same approved pools as Class B: the pools are shared market sources, while the validation implementations and observation retrieval are independent. Using different pools is optional and may reduce comparability if the pools have different liquidity or pricing conditions. + +Two class-A signers run this implementation in separate administrative domains with separate Tezos keys, AWS accounts, IAM roles, secrets, logs, runtime deployments, and artifact hashes. + +### Class B: independent route and validation implementation + +Class B must not reuse Class A's critical acceptance function, policy parser, or compiled validation artifact. It will use a separate implementation language/module boundary and independently maintained parsers and arithmetic code. It may use the same approved source identities and golden vectors, but its source acquisition and validation decisions are independently computed. + +The proposed implementation split is **TypeScript for Class A** and **Rust for Class B**. TypeScript matches the existing CEX publisher and AWS integration inventory. Rust provides a separate type system, parser implementation, arithmetic library, dependency graph, and runtime artifact for the security-critical Class-B path. This is a proposed engineering choice, not a claim that the specification mandates these exact languages. + +The initial Class B design is: + +- independent HTTP clients and schema decoders; +- independently implemented fixed-point decimal parser and median/deviation logic; +- independently maintained source-health and timestamp checks; +- independently maintained DEX quote/TWAP reconstruction from raw observations; +- independent canonical-price calculation; +- comparison against the proposed payload using a class-B fixed signer-deviation limit; +- a separate evidence record keyed by the final payload hash. + +Class-B code may not receive a coordinator-provided price, source result, observation timestamp, tolerance, or policy override as authoritative input. It must retrieve or validate the underlying material locally. + +Class B may read the same approved DEX pools as Class A, but it must use independently operated RPC access where practical, independently decode pool storage and operations, independently calculate executable quotes and TWAPs, and independently apply liquidity, impact, freshness, and cross-pool rules. The security boundary is the independent calculation and decision, not a requirement to invent a different market price by using unrelated pools. + +Operationally, the coordinator first builds a proposal from the approved policy and sends the same proposal to all four signers. Each signer then independently obtains the observations, derives its local price, and either refuses or signs the complete payload. Class A and Class B do not vote on two pieces of one shared calculation: they independently answer whether the complete proposal is correct. The contract accepts only three unique signatures spanning both classes. Thus A1+A2+B1 can pass, but A1+A2+B2 is the same valid class-spanning quorum; A1+A2 alone and A1+B1 alone cannot pass. + +### Isolation and quorum + + +| Signer | Class | Domain | Key/runtime isolation | +| ------ | ----- | --------- | ------------------------------------------------------ | +| A1 | A | Account 1 | Separate root credentials, key, role, deployment, logs | +| A2 | A | Account 2 | Separate root credentials, key, role, deployment, logs | +| B1 | B | Account 3 | Independent implementation and administration | +| B2 | B | Account 4 | Independent implementation and administration | + + +The contract counts unique active keys only. It requires three signatures and at least one from each class. Therefore, losing any one signer preserves liveness, while compromising any two signers cannot produce a quorum. + +### Implementation languages + +The on-chain contract will be authored in SmartPy/Python and compiled to Michelson. SmartPy is the source language for the contract; Tezos executes the resulting Michelson, so the production security boundary is the compiled Michelson artifact and its hash. + +The proposed off-chain implementation is: + +| Component | Proposed language | Reason | +| --- | --- | --- | +| Class A validator and CEX publisher | TypeScript on Node.js | Reuses the existing CEX adapter, AWS Lambda, HTTP, and deployment ecosystem | +| Class B validator | Rust | Provides an independently maintained parser, fixed-point arithmetic path, dependency graph, and compiled runtime artifact | +| Coordinator and permissionless relayer | TypeScript | Shares canonical payload, Tezos RPC, simulation, and submission tooling without holding signing authority | +| Contract tests and compile targets | SmartPy/Python | Matches the existing TezFin contract and test conventions | + +The specification does not mandate TypeScript or Rust. It mandates independent security-critical implementations, separate administration, and no reuse of Class A's acceptance function, policy parser, or compiled validation artifact. Go could replace Rust, or another suitable language could be selected, but changing the proposed language split would require repeating the dependency, packing, arithmetic, signing, and artifact-independence review. The coordinator/relayer language does not create validator independence because it must not decide prices or hold signer keys. + +## 5. Canonical payload proposal + +The logical payload is: + +```text +OraclePayload = { + domain: bytes, + chain_id: bytes, + oracle_address: address, + config_version: nat, + policy_hash: bytes, + publication_group: bytes, + round: nat, + valid_from: timestamp, + valid_until: timestamp, + evidence_digest: bytes, + assets: list +} + +AssetEntry = { + asset_id: string, + price: nat, + decimals: nat, + observation_time: timestamp +} +``` + +The Michelson representation will use a fixed nested pair/record layout with explicit field annotations. The canonical rules are: + +- domain is `TEZFIN_ORACLE_V1`; +- asset entries are sorted by canonical ID; +- publication group determines the exact allowed asset set; +- all five asset IDs have fixed approved decimals, initially six for the TezFin compatibility price; +- no duplicate, unknown, missing, or alternative asset encoding is accepted; +- timestamps are Unix seconds and are checked against the chain time and policy age; +- `valid_until` is bounded by the active policy; +- `observation_time` is the oldest contributing observation for the derived value; +- signatures cover `PACK(OraclePayload)` exactly; +- `BLAKE2B(PACK(OraclePayload))` is the payload/evidence key, not a replacement for signatures. + +The on-chain signature entry will bind each signature to a signer public key and the exact payload bytes. Duplicate keys, inactive keys, wrong classes, malformed signatures, and signatures from another configuration version do not count. + +### Golden vectors + +Before contract implementation, commit vectors containing: + +1. the exact logical payload; +2. the expected Michelson value; +3. packed bytes in hexadecimal; +4. BLAKE2B digest; +5. one valid signature for each supported curve/encoding; +6. expected acceptance/rejection for tampered price, timestamp, round, chain ID, policy hash, and asset order. + +The same vectors will be consumed by the SmartPy tests, both validator classes, the relayer, and the integration test against the compiled TezFin wrapper. + +The golden-vector workflow is deterministic and runs before live source integration: + +1. Create a fixed payload fixture with explicit field values, ordering, timestamps, asset decimals, and policy hash. +2. Serialize it using the proposed Michelson type and pack it with the Tezos `PACK` rules. +3. Record the packed hexadecimal bytes and `BLAKE2B` digest as expected outputs. +4. Sign the exact packed bytes with each supported signer key and record the signatures. +5. Verify the signatures and payload in the contract test, Class A library, Class B library, and relayer. +6. Mutate one field at a time, including price, asset order, decimals, round, chain ID, oracle address, policy hash, and validity window; every mutation must produce a different digest and fail signature or policy validation. +7. Re-run the same fixtures in CI and compare bytes, digests, signatures, and accept/reject results byte-for-byte. + +Vectors are test fixtures, not production prices and not a source of market data. They prove that independently implemented components agree on the signed meaning and that no component silently signs a different serialization. + +## 6. Implementation estimate + +The estimate assumes reuse of the CEX adapter inventory, AWS publisher patterns, Tezos deployment tooling, TezFin wrapper, and existing SmartPy test conventions. Class A and the contract path can be developed in parallel with the Class B design after the payload boundary is frozen. The estimate covers an initial `3-of-4` implementation, not a future `4-of-5` or `5-of-7` expansion. + + +### Stage 1 - contract and deterministic test harness: 2 days + +- implement `ThresholdPriceOracle` storage, signer classes, `3-of-4` threshold, and permissionless submission; +- implement signature, duplicate-signer, policy-hash, group, round, validity-window, and activation-delay checks; +- implement pending/active prices and per-asset pause state; +- add TezFin compatibility views and configuration entrypoints; +- add SmartPy tests for quorum, replay, timestamps, groups, pause, bounds, and same-level activation; +- generate the first canonical payload and golden-vector fixtures; +- compile and record the initial Michelson artifact hash. + +### Stage 2 - Class A publisher and source validation: 2 days + +- reuse and harden Binance, OKX, Kraken, and Coinbase adapters; +- implement schema, timestamp, source-quorum, median, deviation, and freshness checks; +- implement integer fixed-point normalization and USD derivation; +- implement approved USDtz DEX executable quotes, liquidity checks, and TWAP state; +- implement the initial tzBTC peg-factor interface in fail-closed mode; +- add source, arithmetic, DEX, TWAP, retry, idempotency, and malformed-response tests. + +### Stage 3 - Coordinator proposal and relayer protocol: 1 day + +- implement coordinator proposal creation, evidence digest, signing request, and relayer simulation; +- define the coordinator-to-signer proposal and evidence interface; +- ensure signers receive the same proposal but independently retrieve and validate source data; +- allow the primary coordinator to relay its own fully signed payload; +- provide a relayer interface that permits an independent backup relayer to submit the same payload without signing keys; +- add proposal tampering, missing-signature, simulation, retry, and duplicate-submission tests. + +### Stage 4 - Class B independent validator: 3 days + +- implement the Rust source clients and schema decoders independently from Class A; +- implement independent fixed-point parsing, median/deviation logic, timestamp checks, and overflow handling; +- independently decode the approved DEX storage/operations and reconstruct quotes and TWAPs; +- calculate the local canonical proposal and compare it with the coordinator payload; +- implement Class B evidence records, refusal codes, and signing decision; +- run disagreement, one-source-outage, bad-data, stale-data, and two-signer-compromise tests; +- produce the dependency and artifact report proving that Class B does not reuse Class A's critical acceptance code. + +### Stage 5 - deployment and TezFin integration: 2 days + +- configure four isolated signer domains, keys, secrets, roles, logs, and artifact references; +- configure the permissionless relayer and backup RPC path; +- add deployment, verification, and multisig handoff scripts; +- run TezFin wrapper and Comptroller integration tests for all publication groups; +- simulate oracle switching, legacy override removal, pause, recovery, and rollback; +- measure operation size, gas, fee, and latency for the initial `3-of-4` payload. + +### Stage 6 - testnet and shadow launch support: 1 day plus 30 calendar days + +- originate on testnet and verify initial storage and signer configuration; +- run publication, retry, duplicate, signer outage, class disagreement, DEX failure, and missed-heartbeat tests; +- compare accepted prices and timestamps with the independent monitor; +- complete 30 consecutive days using the final production configuration. + +### Review, risks and bugfix + +- Review : **5 hours**; +- Risks: **2 days**; +- Bugfix: **2 days**; + +Based on the results of the work, a security audit should be conducted. Any fixes resulting from the security audit are not included in the estimate. + +### Total estimate + +The implementation work is estimated at **15 days 5 hours**. The 30-day shadow period is calendar time and restarts after any material code, policy, signer, source, mapping, or configuration change. Production activation remains gated by the independent audit and governance approval. + +## 7. Operating-cost estimate + +These are planning ranges and must be replaced by provider quotes and measured chain fees before approval. + +### Four isolated signer domains + +- Four Lambda signer runtimes at 512 MB and up to 120 seconds per five-minute cycle: approximately `$6.3/month` total without relying on independent free-tier allowances; lower with available free tier. +- Four Secrets Manager secrets: approximately `$1.76/month`, excluding unusual KMS usage. +- Optional four customer-managed KMS keys: approximately `$4/month` plus requests. This requires a Tezos-compatible signing integration test before use. +- Four CloudWatch log groups and metrics: approximately `$2-8/month` depending on evidence volume and retention. +- Four small DynamoDB/TWAP or publication-state stores: approximately `$0.10-1/month` at this traffic level, subject to retention and reads. +- EventBridge scheduling and small cross-account coordination traffic: approximately `$0-5/month`. +- External dead-man switch and second alert channel: approximately `$0-20/month`, depending on Healthchecks plan and alert provider. +- Relayer infrastructure: approximately `$0-10/month` for a small backup runtime, excluding XTZ balance. + +Planning total before chain fees is approximately `$15-55/month`. A conservative budget should use the upper range until actual logs, KMS choice, account billing, and alerting are finalized. + +### Tezos fees + +At 8,640 accepted publications per month: + +```text +monthly fees = 8,640 * measured fee per publication +``` + +The earlier `OracleCexOnlyPlan.md` provides the correct initial fee baseline for the single-publisher design: `0.002 XTZ` per publication implies `17.28 XTZ/month`, `0.005 XTZ` implies `43.2 XTZ/month`, and `0.01 XTZ` implies `86.4 XTZ/month` at 8,640 publications per month. Those figures should be retained in the budget as the reference range. They are not yet the threshold-oracle forecast because `CHECK_SIGNATURE`, signer-set checks, larger payloads, and any group/configuration metadata may increase gas and operation fees. The threshold budget should therefore be reported as: + +```text +threshold monthly fee = 8,640 * measured threshold fee per publication +``` + +Until testnet simulation is complete, the CEX-plan range of `17.28-86.4 XTZ/month` is the planning baseline, and the threshold implementation must be shown to fit within or update that range with measured data. Testnet measurements must record gas, fee, operation size, and failure behavior for the initial `3-of-4` configuration and the maximum supported future configuration. + +### Operation byte limit and preliminary size model + +The current Tezos mainnet constants observed from the live RPC are: + +```text +max_operation_data_length = 32,768 bytes (32 KiB) +hard_gas_limit_per_operation = 1,040,000 gas units +hard_gas_limit_per_block = 1,040,000 gas units +cost_per_byte = 250 mutez +``` + +`max_operation_data_length` is the maximum serialized data length accepted for one operation, including its shell and protocol-specific contents. The value is protocol-dependent and must be read again for the target network and protocol. + +`cost_per_byte = 250 mutez` is a storage-allocation cost, not a universal transaction-byte fee and not a fixed gas price. Tezos does not expose one global `mutez per gas` price. The sender chooses an operation fee, and inclusion depends on the active payload producer's fee policy; gas consumption and the explicit fee must therefore be measured separately. + +For a preliminary worst-case model, assume one publication group containing all five canonical assets: + +```text +XTZ_USD, BTC_USD, USDT_USD, USDTZ_USD, TZBTC_USD +``` + +Raw CEX responses, DEX observations, and evidence records are not placed in the operation. The operation contains the derived asset prices, observation timestamps, policy/configuration metadata, and signatures. Using a fixed six-decimal price, Ed25519 signatures, and a full five-asset payload, use these conservative mock values: + +```text +base manager operation and transaction fields = 160 bytes +Michelson parameter/list overhead = 25 bytes +canonical payload, five assets = 450 bytes +one signature entry with signer index = 69 bytes +one signature entry with public key = 105 bytes +``` + +The resulting preliminary sizes are: + +| Configuration | Signatures | Signer indexes | Full public keys | +| --- | ---: | ---: | ---: | +| `3-of-4` | 3 | `160 + 25 + 450 + 3 * 69` = **842 bytes** | `160 + 25 + 450 + 3 * 105` = **950 bytes** | +| `4-of-5` | 4 | `160 + 25 + 450 + 4 * 69` = **911 bytes** | `160 + 25 + 450 + 4 * 105` = **1,055 bytes** | +| `5-of-7` | 5 | `160 + 25 + 450 + 5 * 69` = **980 bytes** | `160 + 25 + 450 + 5 * 105` = **1,160 bytes** | + +With a 30% engineering margin, the `5-of-7` public-key estimate is approximately `1,508 bytes`, or about `1.5 KiB`. Against the current `32,768-byte` operation limit, that mock worst case uses approximately `4.6%` of the available operation data length. This does not prove inclusion: the exact forged operation, Micheline encoding, signature encoding, gas consumption, and baker fee policy still need to be tested. + +The size grows approximately linearly with the number of included signatures: + +```text +size(N) = fixed_operation_and_payload_size + + N * signer_entry_size +``` + +The practical supported configuration is bounded by all relevant constraints: + +```text +N_max = min(limit_by_operation_size, limit_by_operation_gas, + limit_by_block_gas, limit_by_contract_design) +``` + +The dominant threshold constraint may be gas rather than bytes because each signature requires `CHECK_SIGNATURE`. The final benchmark must forge and simulate `3-of-4`, `4-of-5`, `5-of-7`, and the maximum proposed future configuration using the exact compiled contract and worst-case payload. It must record serialized bytes, gas consumed, explicit fee, storage diff, and inclusion result with a production margin. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..cd2fb38 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,69 @@ +# Roadmap + +Authorized work is the **initial 56-hour phase**: public repository, frozen payload/evidence/policy, N-of-M Michelson contract, TypeScript Class A, coordinator/relayer, and local testnet/shadow only. + +Anything not listed under initial scope requires **separate approval**. Passing CI, a testnet origination, or a shadow publication is not production approval. + +## Initial phase (authorized) + +| Workstream | Outcome | +| --- | --- | +| Repository, docs, CI | Public baseline, Apache-2.0, no secrets, passing skeleton CI | +| Payload, evidence, observer agreement, parameter register | Frozen specs; USDT, XTZ, BTC as draft/testnet; USDtz and tzBTC non-authoritative stubs | +| Packing and golden vectors | SmartPy and TypeScript byte-for-byte; signing blocked until green | +| N-of-M contract | Configurable `N`, `M`, class minima; pending/active; delayed unpause/governance; view `(nat, timestamp)` | +| TezFin boundary docs | Upstream price + observation time; aliases/age/bounds remain in TezFin | +| Class A | Adapter interface, two CEX adapters, derivation, candidate verify, testnet signing | +| Coordinator and relayer | Non-authoritative coordinator; permissionless relayer with no keys | +| Local e2e | 1-of-1 observe → derive → sign → simulate/submit → read view, plus negative cases | +| Scope gate | Clean CI, no secrets, stop | + +**Stretch inside remaining hours (not a silent expansion):** live testnet origination, remaining CEX adapters, DEX TWAP, 5-of-7 gas/size benches. + +**Must not happen in this phase:** production activation, Rust Class B, four signer environments, production 3-of-4, TezFin `set_oracle`. + +## Execution order + +Evidence, observer agreement, and the parameter register freeze **with the payload**, before the contract and before any signing path. + +1. Repository foundation (this baseline) +2. Freeze `PAYLOAD_SPEC`, `EVIDENCE_SPEC`, `OBSERVER_AGREEMENT`, `PARAMETER_SCHEMA` and config stubs +3. Packing parity and golden vectors +4. Configurable N-of-M SmartPy contract and harness +5. `ORACLE_INTERFACE.md` TezFin boundary +6. Class A TypeScript validator (may overlap with the contract after packing is frozen) +7. Coordinator and permissionless relayer +8. Local e2e (live origination is stretch) +9. Scope gate and stop + +## Production phase (not authorized) + +These items are recorded so they are not silently pulled into the initial phase: + +- Independent Rust Class B validator +- Proof that Class A and Class B artifacts and operations are independent +- Four separated signer environments: A1, A2, B1, B2 +- Production 3-of-4 with class minima (at least one signature from each class) +- Remaining CEX adapters and DEX TWAP if not finished as stretch +- 30 consecutive days of shadow evidence on the frozen production configuration +- Independent external security audit with no unresolved critical/high findings +- TezFin governance approval of oracle address, asset policies, and market activation +- Authoritative USDtz and tzBTC policies after liquidity and peg review + +## Lifecycle of configuration + +Asset and policy rows in `config/` use explicit lifecycle states: `draft`, `testnet`, `shadow`, `production`. A row marked `production` in a file is not permission to activate TezFin markets. Activation delay applies to policy changes. USDtz and tzBTC remain non-authoritative until separately reviewed. + +## Done when + +The initial phase is done when: + +- CI passes from a clean checkout with no secrets +- Payload vectors are immutable review artifacts +- The contract is N-of-M, not 3-of-4-specific +- Coordinator and relayer have no signing keys +- No TezFin-specific consumer code was added here +- The running system is testnet / non-authoritative shadow only +- Incomplete work is listed under the production phase + +Then **stop** and wait for a separate production authorization. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..653440d --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,104 @@ +# Security + +This document is the public security overview for the **testnet and non-authoritative shadow** phase. It does not replace `TEZFIN_ORACLE_SECURITY_SPECIFICATION_2026_08_11.md` as the production baseline, and it does not grant production approval. + +## Status + +TezOracle is not a production authority. A 1-of-1 configuration, a passing testnet, and a shadow publication are not TezFin borrow, collateral, liquidation, or other price-dependent risk authority. + +Until Class B and four isolated signer environments exist, the system does not meet the production independence model. Treat every price from this phase as non-authoritative. + +## Security objectives + +| Property | Requirement | +| --- | --- | +| Integrity | A requester, coordinator, relayer, RPC, or a single signing key must not be able to force an arbitrary accepted price under the intended production quorum. In 1-of-1 testnet/shadow, that property does not hold — which is why 1-of-1 is forbidden for production. | +| Availability | Loss of one relayer, RPC, or approved CEX source should not block a healthy core update. Availability is never obtained by weakening validation. | +| Freshness | The timestamp exposed downstream is the market observation time used to derive the price. Republishing an old value in a new Tezos operation must not make it fresh. | +| Isolation | USDtz or tzBTC failure must not automatically block XTZ, BTC, or USDt. Those two assets stay non-authoritative until separately reviewed. | +| Auditability | Accepted batches are reproducible from a versioned policy and attributable to a defined signer set. Evidence is retained; secrets are not. | + +## Trust boundaries + +```text +Untrusted + requester, coordinator, relayer, RPC, CEX HTTP, backup relayer + +Trusted only insofar as policy and quorum require + pinned parameter register + Class A derivation / verification (this phase) + N-of-M CHECK_SIGNATURE over frozen PACK(payload) + delayed governance; immediate pause + +Out of scope this phase + Class B independent implementation + A1/A2/B1/B2 operational isolation + production 3-of-4 with class minima +``` + +The coordinator and relayer are **untrusted for price and policy**. They may transport bytes. They may not select sources, deviation, min observations, aggregation, decimals, freshness, or the authoritative price. Validators never accept request-supplied policy. + +## Threat model (initial phase) + +| Threat | Mitigation in this phase | +| --- | --- | +| Coordinator supplies a false price | Validators independently derive or verify under pinned policy; mismatch is fail-closed; no signature. | +| Relayer mutates signed bytes | Relayer must not modify payload; contract verifies `CHECK_SIGNATURE` over exact `PACK(payload)`. | +| Replay on another chain or contract | Payload binds `domain`, `chain_id`, and `oracle_address`. | +| Replay of an old round | Per-group round is strictly monotonic. | +| Stale or future market time | Observation timestamps are checked; inclusion time is not used as freshness. | +| Duplicate or unknown signer | Contract rejects unknown, inactive, and duplicate signer indices. | +| Insufficient quorum | Configurable `N` of `M`; 1-of-1 is testnet/shadow only. | +| Governance foot-gun | Pause is immediate. Unpause, signer-set changes, N/M, class minima, policy hash, and asset changes are delayed. | +| Secrets in git or CI | `.gitignore` excludes key material; `.env.example` is placeholders; CI has no secrets. | +| Silent packing drift | Signing is blocked until SmartPy and TypeScript golden vectors match byte-for-byte. | + +## Failure modes + +The system **fails closed**. If observations are missing, sources disagree beyond policy, evidence mismatches, packing is wrong, the candidate does not match local derivation, quorum is incomplete, or the contract is paused, the update is refused. There is no coordinator override, no degraded “publish anyway” path, and no one-pool USDtz degraded mode in this phase. + +USDtz and tzBTC publication groups may fail independently of `CORE`. They are not consumed as authoritative feeds in this phase. + +## Key handling + +- No production credentials, keys, or endpoints are stored in this repository. +- CI, smoke tests, and the documented quick start do not require production credentials or secrets. +- Testnet signer secrets come from runtime configuration (`.env`, never committed). +- Coordinator and relayer processes hold no signing keys. +- Test-only keys and signatures used for golden vectors are stored separately from any production secret and are not production material. +- Compromising a 1-of-1 testnet key is a full compromise of that deployment. That is an accepted testnet limitation, not a production design. + +## Replay and domain separation + +Every signed payload includes: + +- a domain tag (proposed `TEZORACLE_V1`) +- `chain_id` +- `oracle_address` +- `config_version` and `policy_hash` +- `publication_group` and `round` +- `valid_from` / `valid_until` +- `evidence_digest` and the ordered asset list + +The contract rejects wrong domain, wrong chain, wrong oracle, reused rounds, and signatures that do not match the frozen packed bytes. + +## Pause and governance + +- **Pause** takes effect immediately. +- **Unpause** is delayed. +- Signer set, `N`/`M`, class minima, policy hash, and asset-set changes are delayed. They do not include TezFin `set_oracle` or TezFin aliases. + +## Testnet and shadow limitations + +- This phase uses Class A only. A second implementation class is not present. +- 1-of-1 is allowed only because the feed is non-authoritative. +- Live testnet origination is stretch; local/sandbox e2e is the baseline. +- Shadow output is observable evidence, not a TezFin production feed. +- External audit, 30-day final-config shadow, and TezFin governance activation are production-phase gates. + +## What this repository must never contain + +- Production private keys, mnemonics, or faucet dumps used as production keys +- Production RPC credentials or private endpoints +- Exploit proofs, attack runbooks, or credentials for third-party systems +- TezFin-specific `configureMaxPriceAge` / `configurePriceBounds` duplication as an authority shortcut diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8f3e71a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,570 @@ +{ + "name": "tezoracle", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tezoracle", + "version": "0.0.0", + "license": "Apache-2.0", + "devDependencies": { + "@types/node": "22.20.1", + "tsx": "4.23.12", + "typescript": "5.9.3" + }, + "engines": { + "node": "22.x" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cbb1087 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "tezoracle", + "version": "0.0.0", + "private": true, + "description": "Configurable N-of-M Tezos price oracle. Testnet and non-authoritative shadow only.", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": "22.x" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test tests/validator/smoke.test.ts tests/packing/smoke.test.ts", + "build": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "22.20.1", + "tsx": "4.23.12", + "typescript": "5.9.3" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..875366f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..c3368d1 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +# Exact pins for local and CI. Do not float ranges. +# Install with: python -m pip install -r requirements-dev.txt +iniconfig==2.3.0 +packaging==26.3 +pluggy==1.6.0 +Pygments==2.21.0 +pytest==8.4.2 diff --git a/src/contract/__init__.py b/src/contract/__init__.py new file mode 100644 index 0000000..4ca6ae6 --- /dev/null +++ b/src/contract/__init__.py @@ -0,0 +1,8 @@ +"""SmartPy / Python contract package placeholder. + +The N-of-M Michelson contract is specified in later PRs. This module exists so +the Python CI job has an importable package today. +""" + +__version__ = "0.0.0" +STATUS = "non-production" diff --git a/src/coordinator/index.ts b/src/coordinator/index.ts new file mode 100644 index 0000000..c13ab03 --- /dev/null +++ b/src/coordinator/index.ts @@ -0,0 +1,6 @@ +/** + * Non-authoritative coordinator placeholder. + * + * The coordinator holds no signing keys and does not choose price or policy. + */ +export const COORDINATOR_HOLDS_KEYS = false; diff --git a/src/packing/index.ts b/src/packing/index.ts new file mode 100644 index 0000000..82663aa --- /dev/null +++ b/src/packing/index.ts @@ -0,0 +1,7 @@ +/** + * Canonical payload packing placeholder. + * + * Implementations must not silently reorder or normalize fields. Byte-for-byte + * SmartPy vs TypeScript parity is required before any signing path. + */ +export const PACKING_STATUS = "unfrozen" as const; diff --git a/src/relayer/index.ts b/src/relayer/index.ts new file mode 100644 index 0000000..ea56e27 --- /dev/null +++ b/src/relayer/index.ts @@ -0,0 +1,6 @@ +/** + * Permissionless relayer placeholder. + * + * The relayer must not mutate signed payload bytes and holds no signing keys. + */ +export const RELAYER_HOLDS_KEYS = false; diff --git a/src/validator/index.ts b/src/validator/index.ts new file mode 100644 index 0000000..cdde869 --- /dev/null +++ b/src/validator/index.ts @@ -0,0 +1,8 @@ +/** + * Class A TypeScript validator placeholder. + * + * Signing is blocked until packing golden vectors pass (payload freeze + parity). + * This module exists so TypeScript CI has an importable surface today. + */ +export const TEZORACLE_VERSION = "0.0.0"; +export const STATUS = "non-production" as const; diff --git a/tests/baseline/test_reproducible_baseline.py b/tests/baseline/test_reproducible_baseline.py new file mode 100644 index 0000000..a0a962d --- /dev/null +++ b/tests/baseline/test_reproducible_baseline.py @@ -0,0 +1,45 @@ +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] + + +def test_ci_does_not_use_github_secrets() -> None: + ci = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8") + assert "secrets." not in ci + assert "${{" not in ci or "secrets" not in ci.lower() + + +def test_ci_does_not_upgrade_pip() -> None: + ci = (ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8") + assert "pip install --upgrade pip" not in ci + + +def test_env_example_secret_fields_are_empty() -> None: + text = (ROOT / ".env.example").read_text(encoding="utf-8") + for raw in text.splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + if any(token in key for token in ("SECRET", "KEY", "TOKEN", "PASSWORD")): + assert value == "", f"{key} must stay empty in .env.example" + + +def test_readme_relative_links_exist() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", readme): + if target.startswith(("http://", "https://", "mailto:")): + continue + path = ROOT / target.split("#", 1)[0] + assert path.is_file(), f"README links to missing {target}" + + +def test_readme_omits_docs_absent_from_this_branch() -> None: + readme = (ROOT / "README.md").read_text(encoding="utf-8") + for path in ( + "docs/ENGINEERING_RESPONSE_RU.md", + "docs/TEZFIN_ORACLE_ENGINEERING_RESPONSE_2026_08_14.md", + "IMPLEMENTATION_PLAN.md", + ): + assert f"]({path})" not in readme diff --git a/tests/contract/test_smoke.py b/tests/contract/test_smoke.py new file mode 100644 index 0000000..32390e4 --- /dev/null +++ b/tests/contract/test_smoke.py @@ -0,0 +1,10 @@ +from contract import STATUS, __version__ + + +def test_version_is_defined() -> None: + assert isinstance(__version__, str) + assert len(__version__) > 0 + + +def test_status_is_non_production() -> None: + assert STATUS == "non-production" diff --git a/tests/packing/smoke.test.ts b/tests/packing/smoke.test.ts new file mode 100644 index 0000000..30b95f0 --- /dev/null +++ b/tests/packing/smoke.test.ts @@ -0,0 +1,8 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { PACKING_STATUS } from "../../src/packing/index.js"; + +test("packing is not treated as frozen in the repository skeleton", () => { + assert.equal(PACKING_STATUS, "unfrozen"); +}); diff --git a/tests/validator/smoke.test.ts b/tests/validator/smoke.test.ts new file mode 100644 index 0000000..9e80ca6 --- /dev/null +++ b/tests/validator/smoke.test.ts @@ -0,0 +1,13 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { STATUS, TEZORACLE_VERSION } from "../../src/validator/index.js"; + +test("package version is defined", () => { + assert.equal(typeof TEZORACLE_VERSION, "string"); + assert.ok(TEZORACLE_VERSION.length > 0); +}); + +test("validator status is non-production", () => { + assert.equal(STATUS, "non-production"); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..001c1f8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "noUncheckedIndexedAccess": true, + "types": ["node"] + }, + "include": ["src/**/*.ts", "tests/**/*.ts"] +}