diff --git a/docs/network-v2.md b/docs/network-v2.md index 00f6356..1d124e2 100644 --- a/docs/network-v2.md +++ b/docs/network-v2.md @@ -166,6 +166,40 @@ No automatic migration is provided; separate operator resolution is required. Funding uses signed `EscrowLock` via `POST /v2/runs/{run_id}/escrow/lock`, not a CLI `--bond` flag. Test genesis has pinned nonfinancial origins; production mode has separate reward authority/issuance bounds and no test-genesis conversion. + +Production funding is one coordinator-signed `ProductionBootstrap`, posted as an +`ht/2` envelope to `POST /v2/runs/{run_id}/admin/bootstrap` (admin bearer). The +run's `EconomicsPolicyV2.bootstrap_allocation_hash` pins its owner/units/maturity +list (`genesis_allocation_hash` over `[owner, units, mature_at]`); only +`ledger_mode="production"` may set it, and an unset field is omitted, so earlier +policy digests and run IDs are unchanged. Origins are +`ht-origin-bootstrap-v1|run|allocation_hash|index`. The ledger accepts it once per +run: the same `operation_id` and body replay the stored receipt, the same +`operation_id` with other content answers `OPERATION_CONFLICT`, and any second +bootstrap answers `PRODUCTION_BOOTSTRAP_FORBIDDEN`. It is refused when the inner +authority signature is not `reward_authority` (pinned to `coord_pubkey`), when +the envelope signer is not the coordinator, when the allocation differs from the +pin, when bootstrap plus issued and reserved shadow units would exceed +`max_total_issuance` (`TOTAL_ISSUANCE_BUDGET`; later rewards share the same cap), +when the run has no CUDA qualification (production refuses the CPU backend) and +when the latest beacon round is not BLS-verified (`UNVERIFIED_BEACON`). +Bootstrap units mature on the beacon loop and can be locked or transferred; +like genesis units they are collateral and never pay through `get_weights`. + +Production eligibility also needs the run's `economic-bound` record. `POST +/v2/runs/{run_id}/admin/economic-bound` (admin bearer, no body) writes it once, +coordinator-signed, only when the pinned policy satisfies +`q_floor*s_lower*beta*(R_collectible+S_min) >= G_max` in exact ppm integers +(`challenge/trust_v2.py:economic_bound`); otherwise it answers 409 and writes +nothing. The record binds the economics policy hash; later calls return it +unchanged, and a stored record for another policy answers 409. + +A test-ledger run published on the production challenge steers real emission: +as soon as its first live round finalizes and its reward matures, those units +enter `get_weights` like any production reward +(`tests/challenge/test_v2_weights_bridge.py::test_v2_matured_reward_reaches_get_weights`). +`ledger_mode="test"` does not keep a run out of on-chain weights; do not host a +test-ledger run on the production challenge unless that emission is intended. Rewards are issued only through accepted finality, uniquely tracked by origin, then mature after vesting/dispute conditions. Transfers preserve origin units; fraud burns collectible exposure rather than redistributing it. Shadow work does diff --git a/docs/operator.md b/docs/operator.md index 4b82e00..5f60ece 100644 --- a/docs/operator.md +++ b/docs/operator.md @@ -202,7 +202,7 @@ uv run --frozen python -m hypertrain.ops.manifest_v2 build \ --exp-round 1000000 --state-dir /tmp/state --out /tmp/out ``` -The example's `coord_pubkey`, two auditors, referees, `reward_authority` and relay public key are **placeholder SS58 public identities, not operational keys**. Replace them before a real run; `coord_pubkey` must match the service's K_coord. The example uses a tiny decoder, `ledger_mode="test"`, a pinned genesis allocation hash and one `.test` relay. It creates no dataset, genesis funding or training round. Custom CPU/GPU runs use the same three JSON shapes; provide valid model dimensions and parameter count, an audit budget of `2H`, and the H/step-bound-derived audit lease. Build computes `init_state_hash`; an existing value must match. It also rejects economics whose `reward_authority` differs from `training.coord_pubkey` (`PINNED_AUTHORITIES`) or whose `round_reward_units` differs from `training.budget.epochs_per_round * 1_000_000` (`MANIFEST_REWARD_BUDGET`). The service's configured auditor set must match `training.auditors`; the offline CLI cannot validate that configuration. Building needs the existing `trainer` extra (`uv sync --frozen --extra trainer`); help and example do not. +The example's `coord_pubkey`, two auditors, referees, `reward_authority` and relay public key are **placeholder SS58 public identities, not operational keys**. Replace them before a real run; `coord_pubkey` must match the service's K_coord. The example uses a tiny decoder, `ledger_mode="test"`, a pinned genesis allocation hash and one `.test` relay. It creates no dataset, genesis funding or training round. `example --ledger-mode production` instead writes a production economics policy pinning a placeholder bootstrap allocation (`bootstrap_allocation_hash`, no genesis); funding then uses `POST /v2/runs/{run_id}/admin/bootstrap` and `POST /v2/runs/{run_id}/admin/economic-bound` (see [network-v2.md](network-v2.md)). A test-ledger run hosted on the production challenge steers real emission once its first live reward matures; keep test-mode runs off the production challenge unless that is intended. Custom CPU/GPU runs use the same three JSON shapes; provide valid model dimensions and parameter count, an audit budget of `2H`, and the H/step-bound-derived audit lease. Build computes `init_state_hash`; an existing value must match. It also rejects economics whose `reward_authority` differs from `training.coord_pubkey` (`PINNED_AUTHORITIES`) or whose `round_reward_units` differs from `training.budget.epochs_per_round * 1_000_000` (`MANIFEST_REWARD_BUDGET`). The service's configured auditor set must match `training.auditors`; the offline CLI cannot validate that configuration. Building needs the existing `trainer` extra (`uv sync --frozen --extra trainer`); help and example do not. The owner seed is an existing private, current-user-owned 0600 file containing 32 raw bytes or 64 hex characters; it is read through the miner's checked loader, never printed or copied. The shared loader strips leading/trailing ASCII whitespace bytes: provide hex for any raw seed whose first or last byte is ASCII whitespace. Choose an expiry drand round later than submission. Both commands require a new or empty output directory. Build writes `manifest.json`, `manifest-envelope.json`, five `policy-.json` files, `relay-registry.json` and `index.json`. The index records `run_id`, `owner_ss58`, six canonical object hashes and hashes of the eight other files; it excludes its own hash. diff --git a/src/hypertrain/challenge/app.py b/src/hypertrain/challenge/app.py index 984e787..efc9f86 100644 --- a/src/hypertrain/challenge/app.py +++ b/src/hypertrain/challenge/app.py @@ -570,6 +570,17 @@ async def genesis_v2(run_id: str, request: Request, authorization: Auth = None) admin(authorization) return await run(store.for_run(run_id).genesis_v2, run_id, await v2_raw(request)) + @app.post("/v2/runs/{run_id}/admin/bootstrap") + async def bootstrap_v2(run_id: str, request: Request, authorization: Auth = None) -> Any: + admin(authorization) + hosted = store.for_run(run_id) + return await run(hosted.production_bootstrap_v2, run_id, await v2_raw(request)) + + @app.post("/v2/runs/{run_id}/admin/economic-bound") + async def economic_bound_v2(run_id: str, authorization: Auth = None) -> Any: + admin(authorization) + return await run(store.for_run(run_id).economic_bound_v2, run_id) + @app.post("/v2/runs/{run_id}/admin/rounds") async def open_v2(run_id: str, request: Request, authorization: Auth = None) -> Any: admin(authorization) diff --git a/src/hypertrain/challenge/store.py b/src/hypertrain/challenge/store.py index 176cba1..f1f7914 100644 --- a/src/hypertrain/challenge/store.py +++ b/src/hypertrain/challenge/store.py @@ -4145,6 +4145,54 @@ def genesis_v2(self, run_id: str, raw: bytes) -> dict[str, JsonValue]: ) return receipt.body() + def production_bootstrap_v2(self, run_id: str, raw: bytes) -> dict[str, JsonValue]: + """Pinned production issuance; units mature on the beacon loop, never pay out.""" + from hypertrain.protocol.messages_v2 import ProductionBootstrap + + escrow, admission, _ = self._services(run_id) # production refuses CPU here + with self._tx(): + admission._beacon(self._now(self._db)) # production requires a BLS-verified round + model = self._authenticated_v2(run_id, raw, "ProductionBootstrap", self._coord().ss58) + assert isinstance(model, ProductionBootstrap) + return escrow.production_bootstrap(model).body() + + def economic_bound_v2(self, run_id: str) -> dict[str, JsonValue]: + """Write the immutable coordinator-signed record only when trust_v2 holds the bound.""" + from hypertrain.challenge.trust_v2 import economic_bound + from hypertrain.protocol.messages import Receipt + + escrow, _, _ = self._services(run_id) + with self._tx(): + row = self._db.execute( + "SELECT data FROM records_v2 WHERE run_id=? AND kind='economic-bound' AND id='run'", + (run_id,), + ).fetchone() + if row is not None: + stored: dict[str, JsonValue] = json.loads(row[0]) + if stored["economics_policy_hash"] != escrow.manifest.network.economics_policy_hash: + raise ChallengeError(409, "immutable economic-bound record conflicts") + return stored + policy = escrow.policy + if policy.ledger_mode != "production": + raise ChallengeError(409, "economic-bound record is production only") + if not economic_bound(policy): + raise ChallengeError(409, "economic bound q*s*beta*(R+S)>=G fails") + evidence: dict[str, JsonValue] = { + "run_id": run_id, + "economics_policy_hash": escrow.manifest.network.economics_policy_hash, + "bound": "q*s*beta*(R+S)>=G", + } + now = self._now(self._db) + receipt = Receipt( + w=0, commit_hash=sha256_hex(canonicalize(evidence)), received_round=max(now, 1) + ) + record = { + **evidence, + "receipt": envelope_v2.seal(self._coord(), "Receipt", run_id, receipt, NEVER), + } + self._put_record_v2(run_id, "economic-bound", "run", record) + return record + def _service_profile_v2(self, run_id: str) -> _ServiceCapacityV1: """Derive the sole admitted tiny shape; never allocate tensors.""" manifest = self._run_v2(run_id) diff --git a/src/hypertrain/challenge/trust_v2.py b/src/hypertrain/challenge/trust_v2.py index 9cbb179..ba77a6d 100644 --- a/src/hypertrain/challenge/trust_v2.py +++ b/src/hypertrain/challenge/trust_v2.py @@ -145,6 +145,17 @@ def audit_floor( return AuditFloor(min(q, 1_000_000), required, allowed) +def economic_bound(policy: EconomicsPolicyV2) -> bool: + """Run-level q*s*beta*(R+S)>=G at the pinned audit floor; exact ppm integers.""" + return ( + policy.q_floor + * policy.s_lower_ppm + * policy.beta_ppm + * (policy.R_collectible_units + policy.S_min_units) + >= policy.G_max_units * 10**18 + ) + + @dataclass(frozen=True, slots=True) class AuditSelection: selected: bool diff --git a/src/hypertrain/ledger/escrow_v2.py b/src/hypertrain/ledger/escrow_v2.py index 7564350..22d1e52 100644 --- a/src/hypertrain/ledger/escrow_v2.py +++ b/src/hypertrain/ledger/escrow_v2.py @@ -1,6 +1,7 @@ """Conserved origin units. SQLite event journal and receipts commit with bucket changes. No legacy entitlement import, external deposit, client mint or client slash exists. +Production issuance outside rewards is one coordinator-signed, policy-pinned bootstrap. The service supplies authenticated finality/replay and settlement from its own records. """ @@ -35,6 +36,7 @@ EscrowTransfer, Hex64, OriginAllocation, + ProductionBootstrap, RewardFinalize, RunManifestV2, ShadowReservationRequestV1, @@ -55,7 +57,9 @@ def digest(model: BaseModel) -> str: return body_digest(model.model_dump(mode="json")) -def authority_message(model: TestGenesis | RewardFinalize | ShadowRewardFinalizeV1) -> bytes: +def authority_message( + model: TestGenesis | ProductionBootstrap | RewardFinalize | ShadowRewardFinalizeV1, +) -> bytes: """Origin authority signature is independent of the enclosing envelope signer.""" return ( b"hypertrain/origin/2|" @@ -76,6 +80,10 @@ def genesis_origin_id(run_id: str, allocation_hash: str, index: int) -> str: return sha256_hex(f"ht-origin-genesis-v2|{run_id}|{allocation_hash}|{index}".encode()) +def bootstrap_origin_id(run_id: str, allocation_hash: str, index: int) -> str: + return sha256_hex(f"ht-origin-bootstrap-v1|{run_id}|{allocation_hash}|{index}".encode()) + + def reward_origin_id(run_id: str, w: int, hotkey: str, finalize_hash: str) -> str: return sha256_hex(f"ht-origin-reward-v2|{run_id}|{w}|{hotkey}|{finalize_hash}".encode()) @@ -306,7 +314,9 @@ def _event(self, operation_id: str, kind: str, request: str, owner: str) -> Escr self.db.execute("INSERT INTO escrow_snapshots VALUES(?,?)", (operation_id, state)) return receipt - def _authority(self, model: TestGenesis | RewardFinalize | ShadowRewardFinalizeV1) -> None: + def _authority( + self, model: TestGenesis | ProductionBootstrap | RewardFinalize | ShadowRewardFinalizeV1 + ) -> None: if model.run_id != self.run_id or not verify( decode_hotkey(self.policy.reward_authority), authority_message(model), @@ -351,6 +361,51 @@ def test_genesis(self, genesis: TestGenesis) -> EscrowReceipt: self._issue(o, None, None) return self._event(operation, "TEST_GENESIS", request, genesis.origins[0].owner) + def production_bootstrap(self, bootstrap: ProductionBootstrap) -> EscrowReceipt: + """One pinned production issuance per run; collateral like genesis, never emission.""" + self._authority(bootstrap) + with self.tx(): + request = bootstrap.model_dump_json(exclude={"authority_sig"}) + old = self._existing(bootstrap.operation_id, request) + if old is not None: + return old + if ( + self.policy.ledger_mode != "production" + or self.policy.bootstrap_allocation_hash is None + or self.db.execute( + "SELECT 1 FROM escrow_events WHERE kind='PRODUCTION_BOOTSTRAP'" + ).fetchone() + ): + raise EscrowError("PRODUCTION_BOOTSTRAP_FORBIDDEN") + alloc = [(o.owner, o.units, o.mature_at) for o in bootstrap.origins] + if ( + bootstrap.allocation_hash != self.policy.bootstrap_allocation_hash + or genesis_allocation_hash(alloc) != bootstrap.allocation_hash + ): + raise EscrowError("BOOTSTRAP_ALLOCATION") + pending = self.db.execute( + "SELECT COUNT(*) FROM records_v2 WHERE run_id=? AND kind='shadow-reservation' " + "AND json_extract(data,'$.accepted') IS NULL", + (self.run_id,), + ).fetchone()[0] + if ( + self.balances().issued + + pending * self.policy.round_reward_units + + bootstrap.total_units + > self.policy.max_total_issuance + ): + raise EscrowError("TOTAL_ISSUANCE_BUDGET") + for i, o in enumerate(bootstrap.origins): + if o.origin_id != bootstrap_origin_id(self.run_id, bootstrap.allocation_hash, i): + raise EscrowError("BOOTSTRAP_ORIGIN_DOMAIN") + self._issue(o, None, None) # hotkey NULL: payable() never emits these units + return self._event( + bootstrap.operation_id, + "PRODUCTION_BOOTSTRAP", + request, + bootstrap.origins[0].owner, + ) + def reward_allocations( self, reward: RewardFinalize | ShadowRewardFinalizeV1, evidence: FinalityEvidence ) -> tuple[OriginAllocation, ...]: @@ -973,8 +1028,8 @@ def pay(self, op: EscrowOperation, *, signer: str) -> EscrowReceipt: def payable(self, now_beacon: int) -> list[sqlite3.Row]: """Free units of live reward origins matured by now_beacon, oldest maturity first. - Shadow (bootstrap) and genesis origins are collateral, never emission. Only units still - held by the origin's issued owner pay (its emptied reward_pending row marks it): units + Shadow, production-bootstrap and genesis origins are collateral, never emission. Only units + still held by the origin's issued owner pay (its emptied reward_pending row marks it): units transferred to another owner or a referee are not the earning hotkey's to emit. """ return self.db.execute( diff --git a/src/hypertrain/ops/manifest_v2.py b/src/hypertrain/ops/manifest_v2.py index 7db2d17..b5f7835 100644 --- a/src/hypertrain/ops/manifest_v2.py +++ b/src/hypertrain/ops/manifest_v2.py @@ -153,7 +153,7 @@ def _build(args: argparse.Namespace) -> dict[str, JsonValue]: return index -def _example() -> dict[str, bytes]: +def _example(ledger_mode: str = "test") -> dict[str, bytes]: coord, auditor0, auditor1, referee, relay, cold, other = ( encode_hotkey(bytes([i]) * 32) for i in range(1, 8) ) @@ -197,8 +197,10 @@ def _example() -> dict[str, bytes]: max_object_bytes=1_000_000, max_body_bytes=65536, max_chunk_manifest_bytes=1048576 ), ) + allocation = genesis_allocation_hash([(cold, 20000, 1), (other, 10000, 1)]) + production = ledger_mode == "production" economics = EconomicsPolicyV2( - ledger_mode="test", + ledger_mode="production" if production else "test", G_max_units=100, R_collectible_units=100, S_min_units=1000, @@ -206,11 +208,12 @@ def _example() -> dict[str, bytes]: gammaV_units=0, s_lower_ppm=1_000_000, q_floor=1_000_000, - genesis_allocation_hash=genesis_allocation_hash([(cold, 20000, 1), (other, 10000, 1)]), + genesis_allocation_hash=None if production else allocation, reward_authority=coord, round_reward_units=1_000_000, max_total_issuance=20_000_000, shadow_bootstrap_rounds=12, + bootstrap_allocation_hash=allocation if production else None, ) aggregation = AggregationPolicyV2( arithmetic="flat-cclip-cap-v1", @@ -295,13 +298,14 @@ def main(argv: list[str] | None = None) -> int: build.add_argument("--state-dir", type=Path) example = sub.add_parser("example", help="write tiny CPU inputs with public placeholders") example.add_argument("--out", type=Path, required=True) + example.add_argument("--ledger-mode", choices=("test", "production"), default="test") try: args = parser.parse_args(argv) if args.cmd == "build": result = _build(args) else: _check_out(args.out) - files = _example() + files = _example(args.ledger_mode) _write(args.out, files) result = {"files": {name: sha256_hex(raw) for name, raw in files.items()}} except ValidationError as error: diff --git a/src/hypertrain/protocol/messages_v2.py b/src/hypertrain/protocol/messages_v2.py index def64c3..6fee11a 100644 --- a/src/hypertrain/protocol/messages_v2.py +++ b/src/hypertrain/protocol/messages_v2.py @@ -582,11 +582,15 @@ class EconomicsPolicyV2(WireModel): round_reward_units: U64 max_total_issuance: U64 shadow_bootstrap_rounds: Literal[12] + # Omitted when absent so every policy digest (and run ID) predating it is unchanged. + bootstrap_allocation_hash: Hex64 | None = Field(None, exclude_if=lambda v: v is None) @model_validator(mode="after") def _mode(self) -> EconomicsPolicyV2: if (self.ledger_mode == "test") != (self.genesis_allocation_hash is not None): raise ValueError("only test mode requires pinned genesis allocation") + if self.bootstrap_allocation_hash is not None and self.ledger_mode != "production": + raise ValueError("only production mode pins a bootstrap allocation") return self @@ -730,6 +734,25 @@ def _conservation(self) -> TestGenesis: return self +class ProductionBootstrap(WireModel): + """Coordinator-signed one-time production issuance, pinned by bootstrap_allocation_hash.""" + + run_id: Hex64 + operation_id: Hex64 + allocation_hash: Hex64 + total_units: Positive + origins: Annotated[list[OriginAllocation], Field(min_length=1, max_length=1024)] + authority_sig: Annotated[str, StringConstraints(pattern=r"^[0-9a-f]{128}$")] + + @model_validator(mode="after") + def _conservation(self) -> ProductionBootstrap: + if len({o.origin_id for o in self.origins}) != len(self.origins): + raise ValueError("duplicate bootstrap origin") + if sum(o.units for o in self.origins) != self.total_units: + raise ValueError("bootstrap allocations do not conserve total") + return self + + class RewardFinalize(WireModel): run_id: Hex64 w: U64 @@ -939,6 +962,7 @@ def _loser(self) -> ResolutionV2: DisputePolicyV2, AuditPolicyV2, TestGenesis, + ProductionBootstrap, RewardFinalize, EscrowLock, ShadowRewardFinalizeV1, diff --git a/tests/challenge/test_production_economics_v2.py b/tests/challenge/test_production_economics_v2.py new file mode 100644 index 0000000..c8d583b --- /dev/null +++ b/tests/challenge/test_production_economics_v2.py @@ -0,0 +1,183 @@ +"""Hosted production bootstrap route and economic-bound record writer.""" + +from __future__ import annotations + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +from hypertrain.challenge.admission_store import AdmissionError +from hypertrain.challenge.store import ChallengeError, ChallengeStore +from hypertrain.data.store import LocalFSStore +from hypertrain.ledger import Params +from hypertrain.protocol.envelope_v2 import UnauthorizedSigner, seal, verify_envelope +from hypertrain.protocol.jcs import canonicalize +from hypertrain.protocol.messages_v2 import DisputePolicyV2, RunManifestV2 + +_spec = importlib.util.spec_from_file_location( + "production_economics_fixture", + Path(__file__).parents[1] / "ledger/test_production_bootstrap_v2.py", +) +assert _spec is not None and _spec.loader is not None +B = importlib.util.module_from_spec(_spec) +sys.modules[_spec.name] = B +_spec.loader.exec_module(B) +E = B.E + + +def hosted(tmp_path: Path, monkeypatch, *, cuda: bool, **economics): + """Production run in a real store; `cuda` stands in for an installed CUDA qualification.""" + s = B.pinned(E.setup("production"), **economics) + dispute = DisputePolicyV2( + referees=[E.OTHER.ss58], + max_open=32, + max_transcript_entries=256, + max_entry_bytes=65536, + fanout=2, + max_referee_reassignments=1, + max_referee_cost_units=100, + ) + body = s.manifest.body() + body["network"]["dispute_policy_hash"] = dispute.digest() + manifest = RunManifestV2.model_validate(body) + objects = LocalFSStore(tmp_path / "objects") + for policy in (s.policy, s.admission_policy, dispute): + assert objects.put(canonicalize(policy.body())) == policy.digest() + store = ChallengeStore( + tmp_path / "state", + Params("hypertrain", manifest.training.beacon.genesis_time, 300, 1, 1), + E.COORD, + None, + lambda payload: None, + objects, + ) + store._db.execute( + "INSERT INTO runs VALUES(?,?,?,'created',NULL)", + (manifest.run_id(), manifest.model_dump_json(), "{}"), + ) + if cuda: + monkeypatch.setattr(store, "_backend_v2", lambda m: "cuda") + return store, manifest.run_id() + + +def beacon(store, number: int, *, bls: bool) -> None: + store._db.execute( + "INSERT INTO beacon VALUES(?,?,?,?)", (number, "aa" * 48, "bb" * 32, int(bls)) + ) + + +def signed(run_id: str, signer=E.COORD) -> bytes: + return canonicalize(seal(signer, "ProductionBootstrap", run_id, B.bootstrap(run_id), 10000)) + + +@pytest.fixture +def close(): + stores = [] + yield stores.append + for store in stores: + store.close_v2_notifications() + store.close() + + +def issued(store) -> int: + return store._db.execute("SELECT COALESCE(SUM(issued),0) FROM escrow_origins").fetchone()[0] + + +def test_bootstrap_route_refuses_cpu_backend(tmp_path, monkeypatch, close) -> None: + store, run_id = hosted(tmp_path, monkeypatch, cuda=False) + close(store) + beacon(store, 1, bls=True) + with pytest.raises(ChallengeError, match="production CUDA qualification required"): + store.production_bootstrap_v2(run_id, signed(run_id)) + # Refused before any escrow service (or ledger table) exists for the run. + assert store._services_v2 == {} + assert ( + store._db.execute("SELECT 1 FROM sqlite_master WHERE name='escrow_origins'").fetchone() + is None + ) + + +def test_bootstrap_route_requires_bls_beacon(tmp_path, monkeypatch, close) -> None: + # Given: an unverified (fixture/HTTP-relayed) latest beacon round. + store, run_id = hosted(tmp_path, monkeypatch, cuda=True) + close(store) + beacon(store, 1, bls=False) + # When / Then + with pytest.raises(AdmissionError, match="UNVERIFIED_BEACON"): + store.production_bootstrap_v2(run_id, signed(run_id)) + assert issued(store) == 0 + # A BLS-verified round lets the same signed request through, once. + beacon(store, 2, bls=True) + receipt = store.production_bootstrap_v2(run_id, signed(run_id)) + assert receipt["ledger_mode"] == "production" + assert issued(store) == 30000 + + +def test_bootstrap_route_rejects_wrong_envelope_signer(tmp_path, monkeypatch, close) -> None: + store, run_id = hosted(tmp_path, monkeypatch, cuda=True) + close(store) + beacon(store, 1, bls=True) + with pytest.raises(UnauthorizedSigner): + store.production_bootstrap_v2(run_id, signed(run_id, E.OTHER)) + assert issued(store) == 0 + + +def test_economic_bound_refused_when_bound_false(tmp_path, monkeypatch, close) -> None: + # Given: q*s*beta*(R+S) = 1100 units < G = 1101. + store, run_id = hosted(tmp_path, monkeypatch, cuda=True, G_max_units=1101) + close(store) + beacon(store, 1, bls=True) + _, admission, _ = store._services(run_id) + # When / Then + with pytest.raises(ChallengeError, match="economic bound"): + store.economic_bound_v2(run_id) + assert ( + store._db.execute("SELECT COUNT(*) FROM records_v2 WHERE kind='economic-bound'").fetchone()[ + 0 + ] + == 0 + ) + assert admission.conservative_bound() is False + + +def test_economic_bound_written_once_and_immutable(tmp_path, monkeypatch, close) -> None: + # Given: q*s*beta*(R+S) = 1100 units >= G = 1100 (exact boundary). + store, run_id = hosted(tmp_path, monkeypatch, cuda=True, G_max_units=1100) + close(store) + beacon(store, 1, bls=True) + _, admission, _ = store._services(run_id) + assert admission.conservative_bound() is False + # When + record = store.economic_bound_v2(run_id) + raw = store._db.execute( + "SELECT data FROM records_v2 WHERE kind='economic-bound' AND id='run'" + ).fetchone()[0] + # Then: coordinator-signed, admission now sees the bound. + assert record["receipt"]["signer"] == E.COORD.ss58 and verify_envelope(record["receipt"]) + assert admission.conservative_bound() is True + # A later call (new beacon, fresh randomized signature possible) never rewrites it. + beacon(store, 2, bls=True) + assert store.economic_bound_v2(run_id) == record + after = store._db.execute( + "SELECT data FROM records_v2 WHERE kind='economic-bound' AND id='run'" + ).fetchone()[0] + assert after == raw + # A stored record bound to another policy is a conflict, not silently replaced. + store._db.execute( + "UPDATE records_v2 SET data=json_set(data,'$.economics_policy_hash',?) " + "WHERE kind='economic-bound'", + ("ff" * 32,), + ) + with pytest.raises(ChallengeError, match="immutable economic-bound"): + store.economic_bound_v2(run_id) + assert ( + json.loads( + store._db.execute("SELECT data FROM records_v2 WHERE kind='economic-bound'").fetchone()[ + 0 + ] + )["economics_policy_hash"] + == "ff" * 32 + ) diff --git a/tests/ledger/test_production_bootstrap_v2.py b/tests/ledger/test_production_bootstrap_v2.py new file mode 100644 index 0000000..726cd93 --- /dev/null +++ b/tests/ledger/test_production_bootstrap_v2.py @@ -0,0 +1,260 @@ +"""Production bootstrap issuance: pinned, signed, once per run, collateral never emission.""" + +from __future__ import annotations + +import importlib.util +import sys +from dataclasses import replace +from pathlib import Path + +import pytest +from pydantic import ValidationError + +from hypertrain.ledger.escrow_v2 import ( + Balances, + EscrowError, + Settlement, + authority_message, + bootstrap_origin_id, + digest, + genesis_allocation_hash, +) +from hypertrain.protocol.messages_v2 import ( + EconomicsPolicyV2, + EscrowTransfer, + OriginAllocation, + ProductionBootstrap, + RunManifestV2, +) + +_spec = importlib.util.spec_from_file_location( + "bootstrap_escrow_fixture", Path(__file__).parent / "test_escrow_v2.py" +) +assert _spec is not None and _spec.loader is not None +E = importlib.util.module_from_spec(_spec) +sys.modules[_spec.name] = E +_spec.loader.exec_module(E) + +ALLOC = [(E.COLD.ss58, 20000, 1), (E.OTHER.ss58, 10000, 1)] +OP = "44" * 32 + + +def pinned(s, alloc=ALLOC, **update): + """Production setup whose economics pins `alloc`; the run ID follows the new policy.""" + policy = EconomicsPolicyV2.model_validate( + {**s.policy.body(), "bootstrap_allocation_hash": genesis_allocation_hash(alloc), **update} + ) + body = s.manifest.body() + body["network"]["economics_policy_hash"] = digest(policy) + return replace(s, policy=policy, manifest=RunManifestV2.model_validate(body)) + + +def bootstrap(run_id, alloc=ALLOC, operation=OP, signer=E.COORD) -> ProductionBootstrap: + ah = genesis_allocation_hash(alloc) + unsigned = ProductionBootstrap( + run_id=run_id, + operation_id=operation, + allocation_hash=ah, + total_units=sum(u for _, u, _ in alloc), + origins=[ + OriginAllocation( + origin_id=bootstrap_origin_id(run_id, ah, i), owner=h, units=u, mature_at=m + ) + for i, (h, u, m) in enumerate(alloc) + ], + authority_sig="0" * 128, + ) + return unsigned.model_copy( + update={"authority_sig": signer.sign(authority_message(unsigned)).hex()} + ) + + +def test_bootstrap_conserves_and_survives_restart(tmp_path: Path) -> None: + # Given + s = pinned(E.setup("production")) + e = E.ledger(s, tmp_path / "ledger.db") + b = bootstrap(e.run_id) + # When + e.production_bootstrap(b) + e.mature(E.H, tuple(o.origin_id for o in b.origins), 1) + # Then: per-origin and global conservation hold, also after the chain is re-verified. + assert e.balances() == Balances(30000, 30000, 0, 0, 0, 0, 0) + assert e.balances(E.COLD.ss58).available == 20000 + reopened = E.ledger(s, tmp_path / "ledger.db") + assert reopened.balances().issued == 30000 and reopened.balances().conserved() + kinds = [r["kind"] for r in e.db.execute("SELECT kind FROM escrow_events ORDER BY seq")] + assert kinds == ["PRODUCTION_BOOTSTRAP", "MATURE"] + + +def test_bootstrap_above_total_issuance_rejected(tmp_path: Path) -> None: + s = pinned(E.setup("production"), max_total_issuance=29_999) + e = E.ledger(s, tmp_path / "ledger.db") + with pytest.raises(EscrowError, match="TOTAL_ISSUANCE_BUDGET"): + e.production_bootstrap(bootstrap(e.run_id)) + assert e.balances().issued == 0 + assert e.db.execute("SELECT COUNT(*) FROM escrow_events").fetchone()[0] == 0 + + +def test_bootstrap_idempotent_per_operation_and_once_per_run(tmp_path: Path) -> None: + # Given + s = pinned(E.setup("production")) + e = E.ledger(s, tmp_path / "ledger.db") + b = bootstrap(e.run_id) + first = e.production_bootstrap(b) + # When / Then: exact replay returns the stored receipt and issues nothing more. + assert e.production_bootstrap(bootstrap(e.run_id)) == first + assert e.balances().issued == 30000 + # Same operation_id, different content. + other = [(E.COLD.ss58, 30000, 1)] + with pytest.raises(EscrowError, match="OPERATION_CONFLICT"): + e.production_bootstrap(bootstrap(e.run_id, other)) + # A second bootstrap under a fresh operation_id. + with pytest.raises(EscrowError, match="PRODUCTION_BOOTSTRAP_FORBIDDEN"): + e.production_bootstrap(bootstrap(e.run_id, operation="55" * 32)) + assert e.balances().issued == 30000 + assert e.db.execute("SELECT COUNT(*) FROM escrow_events").fetchone()[0] == 1 + + +@pytest.mark.parametrize("fault", ["signer", "run", "allocation", "unpinned", "test_mode"]) +def test_bootstrap_rejects_unpinned_or_unauthorized(tmp_path: Path, fault: str) -> None: + # Given + s = E.setup("test" if fault == "test_mode" else "production") + if fault != "unpinned" and fault != "test_mode": + s = pinned(s) + e = E.ledger(s, tmp_path / "ledger.db") + run_id = "ab" * 32 if fault == "run" else e.run_id + alloc = [(E.COLD.ss58, 30000, 1)] if fault == "allocation" else ALLOC + b = bootstrap(run_id, alloc, signer=E.OTHER if fault == "signer" else E.COORD) + # When / Then + with pytest.raises(EscrowError): + e.production_bootstrap(b) + assert e.balances().issued == 0 + + +def test_bootstrap_policy_pins_reward_authority_and_mode(tmp_path: Path) -> None: + s = E.setup("production") + # reward_authority must stay the coordinator (escrow_v2 PINNED_AUTHORITIES). + wrong = pinned(s, reward_authority=E.OTHER.ss58) + with pytest.raises(EscrowError, match="PINNED_AUTHORITIES"): + E.ledger(wrong, tmp_path / "ledger.db") + # A test-mode policy cannot carry a bootstrap pin. + test = E.setup("test") + with pytest.raises(ValidationError, match="only production mode pins"): + EconomicsPolicyV2.model_validate( + {**test.policy.body(), "bootstrap_allocation_hash": genesis_allocation_hash(ALLOC)} + ) + # An unpinned policy keeps its pre-existing digest (no new key serialized). + assert "bootstrap_allocation_hash" not in s.policy.body() + + +# One unit over half the fixture's 2e6 cap: bootstrap plus one live award cannot both fit. +BIG = [(E.COLD.ss58, 1_000_001, 1)] + + +@pytest.fixture(scope="module") +def bootstrapped_reward(tmp_path_factory): + """Real signed live w=0 reward (1e6 units) for a run whose policy pins BIG.""" + original = E.setup + try: + E.setup = lambda mode="production": pinned(original(mode), BIG) + return E.actual_reward.__wrapped__(tmp_path_factory) + finally: + E.setup = original + + +def reward_ledger(s, path, reward): + return E.ledger( + s, + path, + replay=E.verified_shadow, + settlement=lambda _: Settlement( + finality_hash=reward.finalize_hash, + closed_dispute_root=E.H, + release_beacon=20, + unresolved=False, + outcome="MATCH", + ), + ) + + +def test_reward_after_bootstrap_cannot_exceed_cap(tmp_path: Path, bootstrapped_reward) -> None: + s, evidence, reward = bootstrapped_reward + assert s.policy.max_total_issuance == 2_000_000 + e = reward_ledger(s, tmp_path / "ledger.db", reward) + e.production_bootstrap(bootstrap(e.run_id, BIG)) + with pytest.raises(EscrowError, match="TOTAL_ISSUANCE_BUDGET"): + e.reward_finalize(reward, evidence) + assert e.balances().issued == 1_000_001 and e.balances().conserved() + assert e.db.execute("SELECT COUNT(*) FROM escrow_finalized").fetchone()[0] == 0 + + +def test_bootstrap_after_reward_cannot_exceed_cap(tmp_path: Path, bootstrapped_reward) -> None: + s, evidence, reward = bootstrapped_reward + e = reward_ledger(s, tmp_path / "ledger.db", reward) + e.reward_finalize(reward, evidence) + with pytest.raises(EscrowError, match="TOTAL_ISSUANCE_BUDGET"): + e.production_bootstrap(bootstrap(e.run_id, BIG)) + assert e.balances().issued == 1_000_000 and e.balances().conserved() + # The matured live award is payable; nothing else was issued. + e.mature(E.H, tuple(reward.origin_ids), 20) + assert [r["origin"] for r in e.payable(10**9)] == reward.origin_ids + + +def test_bootstrap_units_never_payable(tmp_path: Path) -> None: + # Given: matured bootstrap units, some transferred to another owner. + s = pinned(E.setup("production")) + e = E.ledger(s, tmp_path / "ledger.db") + b = bootstrap(e.run_id) + e.production_bootstrap(b) + e.mature(E.H, tuple(o.origin_id for o in b.origins), 1) + e.transfer( + EscrowTransfer( + operation_id="33" * 32, + owner=E.COLD.ss58, + recipient=E.HOT.ss58, + units=500, + origin_ids=[b.origins[0].origin_id], + admission_id=None, + dispute_id=None, + ), + signer=E.COLD.ss58, + ) + # When / Then: free, matured units exist, yet get_weights has nothing to emit. + assert e.balances().available == 30000 + assert e.payable(10**9) == [] + + +def test_bootstrap_exactly_at_total_issuance_is_accepted(tmp_path: Path) -> None: + # Boundary: total == cap passes, 29_999 (above) is rejected in the test above. + s = pinned(E.setup("production"), max_total_issuance=30_000) + e = E.ledger(s, tmp_path / "ledger.db") + e.production_bootstrap(bootstrap(e.run_id)) + assert e.balances().issued == 30_000 and e.balances().conserved() + + +def _pending_reservation(e) -> None: + """A reserved, not yet accepted shadow round: it will mint round_reward_units.""" + e.db.execute( + "INSERT INTO records_v2 VALUES(?,?,?,?)", + (e.run_id, "shadow-reservation", "0", '{"binding":{},"accepted":null}'), + ) + + +@pytest.mark.parametrize("cap_slack,accepted", [(-1, False), (0, True)]) +def test_bootstrap_counts_pending_shadow_reservation_against_cap( + tmp_path: Path, cap_slack: int, accepted: bool +) -> None: + # Given: a pending reservation holds round_reward_units of the cap. + base = E.setup("production") + cap = 30_000 + base.policy.round_reward_units + cap_slack + s = pinned(base, max_total_issuance=cap) + e = E.ledger(s, tmp_path / "ledger.db") + _pending_reservation(e) + # When / Then: bootstrap + reservation must fit; one unit short is refused, exact fit passes. + if accepted: + e.production_bootstrap(bootstrap(e.run_id)) + assert e.balances().issued == 30_000 + else: + with pytest.raises(EscrowError, match="TOTAL_ISSUANCE_BUDGET"): + e.production_bootstrap(bootstrap(e.run_id)) + assert e.balances().issued == 0 diff --git a/tests/ops/test_manifest_v2.py b/tests/ops/test_manifest_v2.py index 69245ba..0332ddb 100644 --- a/tests/ops/test_manifest_v2.py +++ b/tests/ops/test_manifest_v2.py @@ -277,3 +277,15 @@ def test_build_refuses_corrupt_existing_object( assert captured.err == "error: object store contains corrupt content\n" assert path.read_bytes() == b"corrupt" assert not (tmp_path / "output" / "manifest-envelope.json").exists() + + +def test_production_example_pins_bootstrap_not_test_genesis(tmp_path: Path) -> None: + from hypertrain.protocol.messages_v2 import EconomicsPolicyV2 + + out = tmp_path / "production" + assert main(["example", "--ledger-mode", "production", "--out", str(out)]) == 0 + policies = json.loads((out / "policies.json").read_text()) + economics = EconomicsPolicyV2.model_validate(policies["economics"]) + assert economics.ledger_mode == "production" + assert economics.genesis_allocation_hash is None + assert economics.bootstrap_allocation_hash is not None