Bionttestnet
Address

oct2YehV…pjkUvJ

oct2YehVLezCi2RCcSkURc3nyyYtzxmspwGHHALm6pjkUvJ
State
OCT balance
3,809.007OCT
wallet balance
Type
Contract
smart contract on chain
Chain-level
View on Octrascan · devnet
raw txs · nonce · pubkey
Pipoke (no profile)
this wallet has not registered a Pipoke profile
Contract
balance3,809.006698 OCT
version1.0 Rehovot
code hash2a3182…112481
History
live · 20s · last 0
Source · ABI · Bytecode
✓ verified
expand →
✓ verified
// OctraVPN — decentralized private mesh ("Tailscale-on-chain") for Octra.
//
// v1 production AML, validated against the live mainnet
// `octra_compileAml` RPC (see `scripts/compile-check.sh`). Built
// against confirmed Octra primitives only:
//   - runtime: require, transfer, emit, caller, origin, value, epoch,
//     self_addr, checkpoint/commit/rollback, concat, to_string,
//     parse_ints, mget, sha256, len, is_address, ed25519_ok
//   - FHE: fhe_load_pk, fhe_deser, fhe_ser, fhe_add, fhe_sub,
//     fhe_add_const, fhe_scale, fhe_verify_zero
//
// Design highlights:
//   - Operators stake OU in-program (bond_endpoint); both governance
//     slashing (gov_slash_operator) and cryptographic equivocation
//     slashing (slash_double_sign, settle_claim) are available — the
//     latter became possible once Octra confirmed `ed25519_ok` (see
//     `docs/aml-gap-analysis.md`, mainnet reference
//     octBDvZSiTqdEBAyFSp79CHeoLMR9MzHugX9YkHtuQ57MRB).
//   - Tailnets are member groups with shared treasuries; sessions
//     are single-hop with validator-only settle. Receipt integrity
//     is economic (the client's max-pay deposit), not cryptographic.
//   - Earnings accumulate as HFHE ciphertext per operator's pubkey,
//     claimed via two-step (fhe_verify_zero + plain transfer).
//   - IDs are self-incrementing int counters (tailnet_count,
//     session_count) — no SHA256-derived bytes IDs.
//
// See `docs/aml-grammar.md` for the AML grammar reference,
// `docs/aml-gap-analysis.md` for the audit + migration rationale,
// and `docs/whitepaper.md` for the formal claims.

contract OctraVPN {

  // ============================================================
  // Structs
  // ============================================================

  struct EndpointRecord {
    active: int                // 1 once registered; 0 after retire
    endpoint: string           // public address (ip:port or .onion)
    wg_pubkey: string          // X25519 noise pubkey (hex)
    receipt_pubkey: string     // ed25519 pubkey used to sign off-chain
                               // dual-signed receipts (canonical payload:
                               // H("octravpn-receipt-v1" || sid || seq ||
                               // bytes_used || blind)); separate from
                               // the Octra account/tx-signing key.
                               // Stored as hex per docs/keys.md.
    region: string
    price_per_mb: int
    registered_at: int
    reputation: int            // monotonic count of settled sessions
  }

  struct Tailnet {
    owner: address
    treasury: int              // OU available for paid traffic
    member_count: int
    acl_policy: string         // hex of off-chain ACL doc hash
    created_at: int
    exit_count: int
  }

  struct Session {
    tailnet_id: int
    exit: address              // single configured exit (v1: single-hop)
    opener: address            // wallet that called open_session; only it can confirm settlement
    deposit: int               // locked from tailnet treasury at open
    opened_at: int
    status: int                // 0=open, 1=settled, 2=refunded
  }

  /// Operator's or client's settlement claim for a session. Two
  /// of these (one from each side) gate `settle` — see §settle.
  struct ClaimRecord {
    set: int                   // 1 if a claim has been recorded
    bytes_used: int
    claimed_at: int
  }

  // ============================================================
  // Events
  // ============================================================

  event EndpointRegistered(addr: address, endpoint: string, region: string)
  event EndpointUpdated(addr: address)
  event EndpointRetired(addr: address)
  event KeysRotated(addr: address)

  event StakeBonded(addr: address, amount: int, new_stake: int)
  event StakeUnbondingStarted(addr: address, stake: int, unlock_epoch: int)
  event StakeUnbondingFinalized(addr: address, amount: int)
  event OperatorSlashed(addr: address, stake: int, burn_amt: int, bounty_amt: int)

  event TailnetCreated(tailnet_id: int, owner: address)
  event TailnetMemberAdded(tailnet_id: int, member: address)
  event TailnetMemberRemoved(tailnet_id: int, member: address)
  event TailnetDeposit(tailnet_id: int, amount: int, new_treasury: int)
  event TailnetExitConfigured(tailnet_id: int, exit_addr: address)
  event TailnetAclUpdated(tailnet_id: int, acl_policy: string)

  event SessionOpened(session_id: int, tailnet_id: int, exit: address, deposit: int, opened_at: int)
  event SettleClaimed(session_id: int, exit: address, bytes_used: int)
  event SettleConfirmed(session_id: int, opener: address, bytes_used: int)
  event SettleDispute(session_id: int, operator_bytes: int, client_bytes: int)
  event SessionSettled(session_id: int, exit: address, bytes_used: int, total_paid: int, refund: int)
  event SessionRefunded(session_id: int, reason: string)
  event SessionSwept(session_id: int)
  event JoinTokenPrecommitted(tailnet_id: int, token_hash: bytes)
  event JoinTokenRedeemed(tailnet_id: int, member: address, token_hash: bytes)

  event EarningsClaimed(operator: address, amount: int)

  event DeviceRegistered(wallet: address, device: address)
  event DeviceRevoked(wallet: address, device: address)

  event ProgramTreasuryWithdrawn(to: address, amount: int)

  // ============================================================
  // Constants
  // ============================================================

  const MIN_ENDPOINT_STAKE_DEFAULT: int = 1000000000   // 1000 OCT
  const UNBOND_GRACE_DEFAULT: int = 10000              // ~30 days at 10s epochs
  const SLASH_BURN_DEFAULT_BPS: int = 9000             // 90%
  const SLASH_BOUNTY_DEFAULT_BPS: int = 1000           // 10%
  const PROTOCOL_FEE_DEFAULT_BPS: int = 50             // 0.5%
  const SWEEP_GRACE_MULT_DEFAULT: int = 10
  const SWEEP_BOUNTY_DEFAULT_BPS: int = 100            // 1%
  const BPS_DENOM: int = 10000

  // ============================================================
  // State
  // ============================================================

  state {
    owner: address
    paused: int

    // Endpoints
    endpoint_count: int
    endpoint_index: map[int]address          // counter -> addr
    endpoints: map[address]EndpointRecord

    // Operator stake
    endpoint_stake: map[address]int
    endpoint_unbonding_stake: map[address]int
    endpoint_unbonding_unlock: map[address]int
    endpoint_slashed: map[address]int        // 1 = permanently slashed

    // HFHE pubkey + zero-ciphertext per operator, plus the running
    // encrypted earnings ledger.
    op_pk_set: map[address]int               // 1 if operator has registered an HFHE pk
    op_pk: map[address]string
    op_zero_ct: map[address]string
    enc_earnings: map[address]string

    // Tailnets
    tailnet_count: int
    tailnets: map[int]Tailnet
    members: map[int]map[address]int         // tid -> addr -> 1 if member
    tailnet_exits: map[int]map[address]int   // tid -> exit_addr -> 1

    // Sessions
    session_count: int
    sessions: map[int]Session

    // Two-tx settlement: operator's claim + client's confirmation.
    // Settlement only applies when both agree on `bytes_used`. A
    // mismatch records a public dispute (both claims stay on chain).
    operator_claims: map[int]ClaimRecord
    client_confirms: map[int]ClaimRecord

    // Pre-auth join tokens (hash-precommit pattern: owner pre-
    // publishes sha256(preimage); anyone with the preimage redeems).
    // join_token_commits[tid][hash] = 1 means owner published it.
    // join_token_redeemed[hash] = 1 means it's been used once.
    join_token_commits: map[int]map[bytes]int
    join_token_redeemed: map[bytes]int

    // Devices
    device_owner: map[address]address

    // Program treasury (Tier 2 fee + burn share of slashed stakes).
    treasury: int
    burned: int

    // Params (flat — direct struct-field assign isn't supported).
    min_session_deposit: int
    min_tailnet_deposit: int
    session_grace_epochs: int
    sweep_grace_multiplier: int
    sweep_bounty_bps: int
    min_endpoint_stake: int
    unbond_grace_epochs: int
    slash_burn_bps: int
    slash_bounty_bps: int
    protocol_fee_bps: int
  }

  // ============================================================
  // Constructor
  // ============================================================

  constructor(
    initial_min_session_deposit: int,
    initial_min_tailnet_deposit: int,
    initial_session_grace_epochs: int
  ) {
    require(initial_min_session_deposit > 0, "session deposit > 0")
    require(initial_min_tailnet_deposit > 0, "tailnet deposit > 0")
    require(initial_session_grace_epochs > 0, "session grace > 0")

    self.owner = origin
    self.paused = 0
    self.endpoint_count = 0
    self.tailnet_count = 0
    self.session_count = 0
    self.treasury = 0
    self.burned = 0

    self.min_session_deposit = initial_min_session_deposit
    self.min_tailnet_deposit = initial_min_tailnet_deposit
    self.session_grace_epochs = initial_session_grace_epochs
    self.sweep_grace_multiplier = SWEEP_GRACE_MULT_DEFAULT
    self.sweep_bounty_bps = SWEEP_BOUNTY_DEFAULT_BPS
    self.min_endpoint_stake = MIN_ENDPOINT_STAKE_DEFAULT
    self.unbond_grace_epochs = UNBOND_GRACE_DEFAULT
    self.slash_burn_bps = SLASH_BURN_DEFAULT_BPS
    self.slash_bounty_bps = SLASH_BOUNTY_DEFAULT_BPS
    self.protocol_fee_bps = PROTOCOL_FEE_DEFAULT_BPS
  }

  // ============================================================
  // Private helpers
  // ============================================================

  private fn require_not_paused() {
    require(self.paused == 0, "paused")
  }

  private fn require_owner() {
    require(caller == self.owner, "not owner")
  }

  private fn endpoint_is_active(addr: address): bool {
    if self.endpoints[addr].active == 0 {
      return false
    }
    if self.endpoint_slashed[addr] != 0 {
      return false
    }
    if self.endpoint_stake[addr] < self.min_endpoint_stake {
      return false
    }
    return true
  }

  // ============================================================
  // Operator stake (bond / unbond / slash)
  // ============================================================

  fn bond_endpoint(): bool {
    require_not_paused()
    require(value > 0, "no value")
    require(self.endpoint_slashed[caller] == 0, "previously slashed")
    require(self.endpoint_unbonding_stake[caller] == 0, "unbonding in progress")
    self.endpoint_stake[caller] = self.endpoint_stake[caller] + value
    emit StakeBonded(caller, value, self.endpoint_stake[caller])
    return true
  }

  fn unbond_endpoint(): bool {
    require_not_paused()
    let amt = self.endpoint_stake[caller]
    require(amt > 0, "no stake")
    require(self.endpoint_unbonding_stake[caller] == 0, "already unbonding")
    let unlock = epoch + self.unbond_grace_epochs
    self.endpoint_unbonding_stake[caller] = amt
    self.endpoint_unbonding_unlock[caller] = unlock
    self.endpoint_stake[caller] = 0
    if self.endpoints[caller].active != 0 {
      self.endpoints[caller].active = 0
      emit EndpointRetired(caller)
    }
    emit StakeUnbondingStarted(caller, amt, unlock)
    return true
  }

  fn finalize_unbond(): bool {
    require_not_paused()
    let amt = self.endpoint_unbonding_stake[caller]
    require(amt > 0, "no unbonding")
    require(epoch >= self.endpoint_unbonding_unlock[caller], "grace not elapsed")
    self.endpoint_unbonding_stake[caller] = 0
    self.endpoint_unbonding_unlock[caller] = 0
    require(transfer(caller, amt), "transfer failed")
    emit StakeUnbondingFinalized(caller, amt)
    return true
  }

  // Cryptographic equivocation slash.
  //
  // The off-chain dual-signed-receipt protocol (see
  // `crates/octravpn-core/src/receipt.rs`, canonical payload:
  // H("octravpn-receipt-v1" || session_id || seq || bytes_used ||
  // blind)) makes the operator's `receipt_pubkey` a non-repudiation
  // anchor. An honest operator never signs two different payloads
  // under that key; if anyone presents two such payloads + sigs the
  // operator is provably equivocating, regardless of what those
  // payloads encode. AML's `ed25519_ok(pk, msg, sig)` host call lets
  // us verify both signatures in-program (confirmed by the Octra dev
  // team 2026-05-14; reference deployment
  // octBDvZSiTqdEBAyFSp79CHeoLMR9MzHugX9YkHtuQ57MRB).
  //
  // The `session_id` parameter is the *claimed* session id; we do
  // NOT parse the payload to enforce it (AML lacks a byte-slicer).
  // The slasher built both payloads from real off-chain receipts, so
  // the sig-verify gate already ensures they're real receipts under
  // the operator's receipt key. Any two distinct signed payloads
  // under one receipt key are evidence enough — the operator loses
  // its bond and the caller gets the bounty (90 % burn / 10 %
  // bounty, mirroring `gov_slash_operator`).
  fn slash_double_sign(
    operator_addr: address,
    session_id: int,
    payload_a: string,
    sig_a: string,
    payload_b: string,
    sig_b: string
  ): bool {
    require_not_paused()
    require(self.endpoint_slashed[operator_addr] == 0, "already slashed")
    require(payload_a != payload_b, "payloads identical")
    let receipt_pk = self.endpoints[operator_addr].receipt_pubkey
    require(len(receipt_pk) > 0, "operator has no receipt pubkey")
    require(ed25519_ok(receipt_pk, payload_a, sig_a), "sig_a invalid")
    require(ed25519_ok(receipt_pk, payload_b, sig_b), "sig_b invalid")

    let live = self.endpoint_stake[operator_addr]
    let unb = self.endpoint_unbonding_stake[operator_addr]
    let total = live + unb
    require(total > 0, "no stake to slash")

    let burn_amt = total * self.slash_burn_bps / BPS_DENOM
    let bounty_amt = total - burn_amt

    self.endpoint_stake[operator_addr] = 0
    self.endpoint_unbonding_stake[operator_addr] = 0
    self.endpoint_unbonding_unlock[operator_addr] = 0
    self.endpoint_slashed[operator_addr] = 1
    if self.endpoints[operator_addr].active != 0 {
      self.endpoints[operator_addr].active = 0
    }
    self.treasury = self.treasury + burn_amt
    if bounty_amt > 0 {
      require(transfer(caller, bounty_amt), "bounty transfer failed")
    }
    emit OperatorSlashed(operator_addr, total, burn_amt, bounty_amt)
    return true
  }

  // Governance slash. Complements the cryptographic equivocation
  // slashes (`slash_double_sign`, in-AML equivocation in
  // `settle_claim`). The owner submits a slash backed by off-chain
  // evidence verification (`octravpn slash-evidence verify`).
  fn gov_slash_operator(operator_addr: address): bool {
    require_not_paused()
    require_owner()
    require(self.endpoint_slashed[operator_addr] == 0, "already slashed")
    let live = self.endpoint_stake[operator_addr]
    let unb = self.endpoint_unbonding_stake[operator_addr]
    let total = live + unb
    require(total > 0, "no stake to slash")

    let burn_amt = total * self.slash_burn_bps / BPS_DENOM
    let bounty_amt = total - burn_amt

    self.endpoint_stake[operator_addr] = 0
    self.endpoint_unbonding_stake[operator_addr] = 0
    self.endpoint_unbonding_unlock[operator_addr] = 0
    self.endpoint_slashed[operator_addr] = 1
    if self.endpoints[operator_addr].active != 0 {
      self.endpoints[operator_addr].active = 0
    }
    self.treasury = self.treasury + burn_amt
    if bounty_amt > 0 {
      require(transfer(caller, bounty_amt), "bounty transfer failed")
    }
    emit OperatorSlashed(operator_addr, total, burn_amt, bounty_amt)
    return true
  }

  // ============================================================
  // Endpoint lifecycle
  // ============================================================

  fn register_endpoint(
    endpoint: string,
    wg_pubkey: string,
    hfhe_pubkey: string,
    initial_enc_zero: string,
    region: string,
    price_per_mb: int,
    receipt_pubkey: string
  ): bool {
    require_not_paused()
    require(self.endpoint_slashed[caller] == 0, "previously slashed")
    require(
      self.endpoint_stake[caller] >= self.min_endpoint_stake,
      "must bond_endpoint first"
    )
    require(price_per_mb > 0, "price > 0")
    require(len(wg_pubkey) > 0, "wg pubkey required")
    require(len(hfhe_pubkey) > 0, "hfhe pubkey required")
    require(len(initial_enc_zero) > 0, "initial enc(0) required")
    require(len(receipt_pubkey) > 0, "receipt pubkey required")
    require(caller == origin, "direct caller only")
    require(self.endpoints[caller].active == 0, "already registered")

    self.op_pk_set[caller] = 1
    self.op_pk[caller] = hfhe_pubkey
    self.op_zero_ct[caller] = initial_enc_zero
    self.enc_earnings[caller] = initial_enc_zero

    self.endpoints[caller].active = 1
    self.endpoints[caller].endpoint = endpoint
    self.endpoints[caller].wg_pubkey = wg_pubkey
    self.endpoints[caller].receipt_pubkey = receipt_pubkey
    self.endpoints[caller].region = region
    self.endpoints[caller].price_per_mb = price_per_mb
    self.endpoints[caller].registered_at = epoch
    self.endpoints[caller].reputation = 0

    self.endpoint_index[self.endpoint_count] = caller
    self.endpoint_count = self.endpoint_count + 1

    emit EndpointRegistered(caller, endpoint, region)
    return true
  }

  fn update_endpoint(endpoint: string, region: string, price_per_mb: int): bool {
    require_not_paused()
    require(self.endpoints[caller].active != 0, "not registered")
    require(price_per_mb > 0, "price > 0")
    self.endpoints[caller].endpoint = endpoint
    self.endpoints[caller].region = region
    self.endpoints[caller].price_per_mb = price_per_mb
    emit EndpointUpdated(caller)
    return true
  }

  // Rotating HFHE keys requires the operator to have claimed all
  // outstanding earnings first — the existing ciphertext is
  // encrypted under the OLD key and won't decrypt under the new.
  fn rotate_keys(
    new_wg_pubkey: string,
    new_hfhe_pubkey: string,
    new_initial_enc_zero: string
  ): bool {
    require_not_paused()
    require(self.endpoints[caller].active != 0, "not registered")
    require(caller == origin, "direct caller only")
    require(len(new_wg_pubkey) > 0, "wg pubkey required")
    require(len(new_hfhe_pubkey) > 0, "hfhe pubkey required")
    require(len(new_initial_enc_zero) > 0, "initial enc(0) required")
    // Refuse rotation while earnings are non-zero.
    require(self.enc_earnings[caller] == self.op_zero_ct[caller], "claim earnings first")
    self.endpoints[caller].wg_pubkey = new_wg_pubkey
    self.op_pk[caller] = new_hfhe_pubkey
    self.op_zero_ct[caller] = new_initial_enc_zero
    self.enc_earnings[caller] = new_initial_enc_zero
    emit KeysRotated(caller)
    return true
  }

  fn retire_endpoint(): bool {
    require_not_paused()
    require(self.endpoints[caller].active != 0, "not registered")
    self.endpoints[caller].active = 0
    emit EndpointRetired(caller)
    return true
  }

  // ============================================================
  // Tailnet lifecycle
  // ============================================================

  fn create_tailnet(acl_policy: string): int {
    require_not_paused()
    require(value >= self.min_tailnet_deposit, "tailnet deposit below min")
    require(len(acl_policy) > 0, "acl policy required")

    let tid = self.tailnet_count
    self.tailnet_count = self.tailnet_count + 1

    self.tailnets[tid].owner = caller
    self.tailnets[tid].treasury = value
    self.tailnets[tid].member_count = 1
    self.tailnets[tid].acl_policy = acl_policy
    self.tailnets[tid].created_at = epoch
    self.tailnets[tid].exit_count = 0

    // Owner is implicitly the first member.
    self.members[tid][caller] = 1

    emit TailnetCreated(tid, caller)
    emit TailnetMemberAdded(tid, caller)
    return tid
  }

  fn add_member(tailnet_id: int, member: address): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(self.tailnets[tailnet_id].owner == caller, "not tailnet owner")
    require(self.members[tailnet_id][member] == 0, "already member")
    self.members[tailnet_id][member] = 1
    self.tailnets[tailnet_id].member_count = self.tailnets[tailnet_id].member_count + 1
    emit TailnetMemberAdded(tailnet_id, member)
    return true
  }

  fn remove_member(tailnet_id: int, member: address): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(self.tailnets[tailnet_id].owner == caller, "not tailnet owner")
    require(member != self.tailnets[tailnet_id].owner, "cannot remove owner")
    require(self.members[tailnet_id][member] == 1, "not member")
    self.members[tailnet_id][member] = 0
    self.tailnets[tailnet_id].member_count = self.tailnets[tailnet_id].member_count - 1
    emit TailnetMemberRemoved(tailnet_id, member)
    return true
  }

  fn deposit_to_tailnet(tailnet_id: int): bool {
    require_not_paused()
    require(value > 0, "no value")
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    self.tailnets[tailnet_id].treasury = self.tailnets[tailnet_id].treasury + value
    let new_t = self.tailnets[tailnet_id].treasury
    emit TailnetDeposit(tailnet_id, value, new_t)
    return true
  }

  fn configure_tailnet_exit(tailnet_id: int, exit_addr: address): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(self.tailnets[tailnet_id].owner == caller, "not tailnet owner")
    require(self.endpoints[exit_addr].active != 0, "exit not registered")
    require(endpoint_is_active(exit_addr), "exit inactive")
    require(self.tailnet_exits[tailnet_id][exit_addr] == 0, "already configured")
    self.tailnet_exits[tailnet_id][exit_addr] = 1
    self.tailnets[tailnet_id].exit_count = self.tailnets[tailnet_id].exit_count + 1
    emit TailnetExitConfigured(tailnet_id, exit_addr)
    return true
  }

  fn update_acl(tailnet_id: int, new_acl_policy: string): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(self.tailnets[tailnet_id].owner == caller, "not tailnet owner")
    require(len(new_acl_policy) > 0, "acl required")
    self.tailnets[tailnet_id].acl_policy = new_acl_policy
    emit TailnetAclUpdated(tailnet_id, new_acl_policy)
    return true
  }

  // ============================================================
  // Pre-auth join tokens (hash-precommit pattern).
  // ============================================================
  //
  // We can't verify an off-chain Ed25519 signature inside AML, so
  // we substitute a hash-precommit: the tailnet owner publishes
  // `sha256(token_preimage)` on chain via `precommit_join_token`;
  // anyone holding the preimage redeems via `redeem_join_token`.
  //
  // Trade-off vs the v0 design: the token hash is publicly visible
  // on chain at issue time (chain observers see "this tailnet has
  // a pending invite"), but no signature verification is needed.
  // The preimage is shared off-chain (out-of-band) with the
  // intended joiner.
  //
  // Tokens are one-shot: once redeemed, the hash is marked spent
  // and can't be redeemed again.

  fn precommit_join_token(tailnet_id: int, token_hash: bytes): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(self.tailnets[tailnet_id].owner == caller, "not tailnet owner")
    require(len(token_hash) == 32, "token hash must be 32B (sha256)")
    require(self.join_token_commits[tailnet_id][token_hash] == 0, "already committed")
    require(self.join_token_redeemed[token_hash] == 0, "hash already used")
    self.join_token_commits[tailnet_id][token_hash] = 1
    emit JoinTokenPrecommitted(tailnet_id, token_hash)
    return true
  }

  fn redeem_join_token(tailnet_id: int, token_preimage: bytes): bool {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")
    require(len(token_preimage) > 0, "preimage required")
    let h = sha256(token_preimage)
    require(self.join_token_commits[tailnet_id][h] == 1, "unknown token")
    require(self.join_token_redeemed[h] == 0, "already redeemed")
    require(self.members[tailnet_id][caller] == 0, "already member")
    self.members[tailnet_id][caller] = 1
    self.tailnets[tailnet_id].member_count = self.tailnets[tailnet_id].member_count + 1
    self.join_token_redeemed[h] = 1
    emit TailnetMemberAdded(tailnet_id, caller)
    emit JoinTokenRedeemed(tailnet_id, caller, h)
    return true
  }

  // ============================================================
  // Device registry
  // ============================================================

  fn register_device(device: address): bool {
    require_not_paused()
    require(is_address(device), "bad device addr")
    let existing = self.device_owner[device]
    if existing == caller {
      return true
    }
    require(existing == 0, "device attached to another wallet")
    self.device_owner[device] = caller
    emit DeviceRegistered(caller, device)
    return true
  }

  fn revoke_device(device: address): bool {
    require_not_paused()
    require(self.device_owner[device] == caller, "not device owner")
    self.device_owner[device] = 0
    emit DeviceRevoked(caller, device)
    return true
  }

  view fn get_device_owner(device: address): address {
    return self.device_owner[device]
  }

  view fn is_device_of(device: address, wallet: address): bool {
    return self.device_owner[device] == wallet
  }

  // ============================================================
  // Session lifecycle (single-hop)
  // ============================================================

  // Open a single-hop session against a configured tailnet exit.
  // The client deposits `max_pay` from the tailnet treasury — this
  // is the ceiling the validator can ever extract.
  fn open_session(
    tailnet_id: int,
    exit_addr: address,
    max_pay: int
  ): int {
    require_not_paused()
    require(tailnet_id < self.tailnet_count, "tailnet not found")

    // Membership: caller is a member directly, OR caller is a
    // device whose owner is a member.
    let direct = self.members[tailnet_id][caller]
    if direct == 0 {
      let owner_wallet = self.device_owner[caller]
      require(owner_wallet != 0, "not a member")
      require(self.members[tailnet_id][owner_wallet] == 1, "device's owner is not a member")
    }

    require(self.tailnet_exits[tailnet_id][exit_addr] == 1, "exit not configured")
    require(endpoint_is_active(exit_addr), "exit inactive")

    require(max_pay >= self.min_session_deposit, "deposit below min")
    require(self.tailnets[tailnet_id].treasury >= max_pay, "treasury insufficient")

    // Lock deposit from treasury.
    self.tailnets[tailnet_id].treasury = self.tailnets[tailnet_id].treasury - max_pay

    let sid = self.session_count
    self.session_count = self.session_count + 1

    self.sessions[sid].tailnet_id = tailnet_id
    self.sessions[sid].exit = exit_addr
    self.sessions[sid].opener = caller
    self.sessions[sid].deposit = max_pay
    self.sessions[sid].opened_at = epoch
    self.sessions[sid].status = 0

    emit SessionOpened(sid, tailnet_id, exit_addr, max_pay, epoch)
    return sid
  }

  // ============================================================
  // Two-tx settle: operator claims, client confirms.
  // ============================================================
  //
  // Octra's runtime ed25519-verifies every tx before AML sees it,
  // so when `settle_claim` fires we know the operator's signature
  // is good, and when `settle_confirm` fires we know the client's
  // signature is good. Two on-chain txs give us cryptographic
  // dual-sig without an in-AML `verify_ed25519` host call.
  //
  // The state machine:
  //   1. Operator calls `settle_claim(sid, bytes_used)`. AML records
  //      the claim. If the operator already claimed a DIFFERENT
  //      value for this session, that's equivocation and the
  //      operator is slashed atomically right here.
  //   2. Client (the session opener) calls
  //      `settle_confirm(sid, bytes_used)` matching the operator's
  //      claim → settlement applies (FHE earnings credit, refund,
  //      fee). Mismatch → a public `SettleDispute` event with both
  //      values; settlement does NOT apply.
  //   3. If the client never confirms within grace, `claim_no_show`
  //      refunds the deposit and the operator gets nothing.
  //
  // The dispute records (both signed txs) stay on chain as evidence
  // for off-chain reputation / dispute-resolution systems.

  fn settle_claim(session_id: int, bytes_used: int): bool {
    require_not_paused()
    require(session_id < self.session_count, "session not found")
    require(self.sessions[session_id].status == 0, "session not open")
    require(bytes_used >= 0, "bytes >= 0")
    require(caller == self.sessions[session_id].exit, "not session exit")
    require(endpoint_is_active(caller), "operator inactive")

    let prev_set = self.operator_claims[session_id].set
    if prev_set == 1 {
      let prev_bytes = self.operator_claims[session_id].bytes_used
      if prev_bytes == bytes_used {
        // Idempotent re-claim (network retry etc.): no-op.
        return true
      }
      // Equivocation: same operator, same session, different bytes.
      // Slash atomically using the same logic as gov_slash_operator
      // (90 % burn, 10 % bounty to submitter, permanent flag).
      let live = self.endpoint_stake[caller]
      let unb = self.endpoint_unbonding_stake[caller]
      let total = live + unb
      let burn_amt = total * self.slash_burn_bps / BPS_DENOM
      let bounty_amt = total - burn_amt
      self.endpoint_stake[caller] = 0
      self.endpoint_unbonding_stake[caller] = 0
      self.endpoint_unbonding_unlock[caller] = 0
      self.endpoint_slashed[caller] = 1
      if self.endpoints[caller].active != 0 {
        self.endpoints[caller].active = 0
      }
      self.treasury = self.treasury + burn_amt
      // No bounty transfer here — caller IS the operator. The
      // bounty is forfeited (it stays in self.treasury via burn).
      self.treasury = self.treasury + bounty_amt
      emit OperatorSlashed(caller, total, burn_amt + bounty_amt, 0)
      // Force-refund the session deposit to the tailnet treasury;
      // the session can't be settled by a slashed operator.
      self.sessions[session_id].status = 2
      let tid_eq = self.sessions[session_id].tailnet_id
      let dep_eq = self.sessions[session_id].deposit
      self.tailnets[tid_eq].treasury = self.tailnets[tid_eq].treasury + dep_eq
      emit SessionRefunded(session_id, "operator-equivocation")
      return false
    }

    self.operator_claims[session_id].set = 1
    self.operator_claims[session_id].bytes_used = bytes_used
    self.operator_claims[session_id].claimed_at = epoch
    emit SettleClaimed(session_id, caller, bytes_used)
    return true
  }

  // Client-side confirmation. The session opener (whoever called
  // open_session) is the only address that can confirm.
  fn settle_confirm(session_id: int, bytes_used: int): bool {
    require_not_paused()
    require(session_id < self.session_count, "session not found")
    require(self.sessions[session_id].status == 0, "session not open")
    require(bytes_used >= 0, "bytes >= 0")
    require(caller == self.sessions[session_id].opener, "not session opener")

    let op_set = self.operator_claims[session_id].set
    require(op_set == 1, "operator has not claimed yet")
    let op_bytes = self.operator_claims[session_id].bytes_used

    if op_bytes != bytes_used {
      // Disagreement. Record the dispute publicly. Settlement does
      // NOT apply — either party can later call claim_no_show after
      // grace to refund the deposit (operator gets nothing).
      self.client_confirms[session_id].set = 1
      self.client_confirms[session_id].bytes_used = bytes_used
      self.client_confirms[session_id].claimed_at = epoch
      emit SettleDispute(session_id, op_bytes, bytes_used)
      return false
    }

    // Match — apply the settlement.
    let exit_addr = self.sessions[session_id].exit
    require(endpoint_is_active(exit_addr), "operator inactive")
    let price = self.endpoints[exit_addr].price_per_mb
    let total_paid = bytes_used * price
    let deposit = self.sessions[session_id].deposit
    require(total_paid <= deposit, "claim exceeds escrow")

    let protocol_fee = total_paid * self.protocol_fee_bps / BPS_DENOM
    let net_pay = total_paid - protocol_fee
    let refund = deposit - total_paid

    self.sessions[session_id].status = 1
    self.client_confirms[session_id].set = 1
    self.client_confirms[session_id].bytes_used = bytes_used
    self.client_confirms[session_id].claimed_at = epoch

    if net_pay > 0 {
      let pk = fhe_load_pk(exit_addr)
      let zero_ct = fhe_deser(self.op_zero_ct[exit_addr])
      let pay_ct = fhe_add_const(pk, zero_ct, net_pay)
      let cur = fhe_deser(self.enc_earnings[exit_addr])
      let new_enc = fhe_add(pk, cur, pay_ct)
      self.enc_earnings[exit_addr] = fhe_ser(new_enc)
    }

    self.endpoints[exit_addr].reputation = self.endpoints[exit_addr].reputation + 1
    if protocol_fee > 0 {
      self.treasury = self.treasury + protocol_fee
    }

    if refund > 0 {
      let tid = self.sessions[session_id].tailnet_id
      self.tailnets[tid].treasury = self.tailnets[tid].treasury + refund
    }

    emit SettleConfirmed(session_id, caller, bytes_used)
    emit SessionSettled(session_id, exit_addr, bytes_used, total_paid, refund)
    return true
  }

  fn claim_no_show(session_id: int): bool {
    require_not_paused()
    require(session_id < self.session_count, "session not found")
    require(self.sessions[session_id].status == 0, "session not open")
    let opened = self.sessions[session_id].opened_at
    require(epoch >= opened + self.session_grace_epochs, "grace not elapsed")

    self.sessions[session_id].status = 2
    let tid = self.sessions[session_id].tailnet_id
    let dep = self.sessions[session_id].deposit
    self.tailnets[tid].treasury = self.tailnets[tid].treasury + dep
    emit SessionRefunded(session_id, "no-show")
    return true
  }

  fn sweep_expired_session(session_id: int): bool {
    require_not_paused()
    require(session_id < self.session_count, "session not found")
    require(self.sessions[session_id].status == 0, "session not open")
    let opened = self.sessions[session_id].opened_at
    let sweep_grace = self.session_grace_epochs * self.sweep_grace_multiplier
    require(epoch >= opened + sweep_grace, "sweep grace not elapsed")

    self.sessions[session_id].status = 2
    let dep = self.sessions[session_id].deposit
    let bounty = dep * self.sweep_bounty_bps / BPS_DENOM
    let refund = dep - bounty
    if bounty > 0 {
      require(transfer(caller, bounty), "bounty transfer failed")
    }
    if refund > 0 {
      let tid = self.sessions[session_id].tailnet_id
      self.tailnets[tid].treasury = self.tailnets[tid].treasury + refund
    }
    emit SessionSwept(session_id)
    return true
  }

  // ============================================================
  // Earnings claim (two-step: FHE verify + plain transfer)
  // ============================================================

  view fn get_encrypted_earnings(addr: address): string {
    return self.enc_earnings[addr]
  }

  // Two-step claim. AML verifies the operator's FHE zero-proof
  // that `enc_earnings - enc(claimed_amount) = enc(0)`, zeroes the
  // ledger, and transfers plaintext OU. The operator's wallet then
  // wraps the funds in a native op_type="stealth" tx (off-AML) for
  // unlinkable payout.
  //
  // Partial claims are not supported in v1; the operator must
  // claim the exact accumulated total.
  fn claim_earnings(claimed_amount: int, proof: string): bool {
    require_not_paused()
    require(self.endpoint_slashed[caller] == 0, "operator slashed")
    require(self.op_pk_set[caller] == 1, "no hfhe pubkey")
    require(claimed_amount > 0, "amount > 0")
    require(len(proof) > 0, "proof required")

    let pk = fhe_load_pk(caller)
    let zero_ct = fhe_deser(self.op_zero_ct[caller])
    let claim_ct = fhe_add_const(pk, zero_ct, claimed_amount)
    let cur = fhe_deser(self.enc_earnings[caller])
    let delta = fhe_sub(pk, cur, claim_ct)
    require(fhe_verify_zero(pk, delta, proof), "bad opening")

    // Reset and pay out.
    self.enc_earnings[caller] = self.op_zero_ct[caller]
    require(transfer(caller, claimed_amount), "transfer failed")
    emit EarningsClaimed(caller, claimed_amount)
    return true
  }

  // ============================================================
  // Views
  // ============================================================

  view fn get_endpoint(addr: address): EndpointRecord {
    return self.endpoints[addr]
  }

  view fn get_endpoint_stake(addr: address): int {
    return self.endpoint_stake[addr]
  }

  view fn get_endpoint_unbonding(addr: address): int {
    return self.endpoint_unbonding_stake[addr]
  }

  view fn get_endpoint_unlock(addr: address): int {
    return self.endpoint_unbonding_unlock[addr]
  }

  view fn is_endpoint_slashed(addr: address): bool {
    return self.endpoint_slashed[addr] != 0
  }

  view fn get_tailnet(tailnet_id: int): Tailnet {
    return self.tailnets[tailnet_id]
  }

  view fn is_tailnet_member(tailnet_id: int, addr: address): bool {
    return self.members[tailnet_id][addr] == 1
  }

  view fn is_tailnet_exit(tailnet_id: int, addr: address): bool {
    return self.tailnet_exits[tailnet_id][addr] == 1
  }

  view fn get_session(session_id: int): Session {
    return self.sessions[session_id]
  }

  view fn get_endpoint_at(index: int): address {
    return self.endpoint_index[index]
  }

  view fn endpoint_count_view(): int {
    return self.endpoint_count
  }

  view fn tailnet_count_view(): int {
    return self.tailnet_count
  }

  view fn session_count_view(): int {
    return self.session_count
  }

  view fn get_program_treasury(): int {
    return self.treasury
  }

  view fn get_min_endpoint_stake(): int {
    return self.min_endpoint_stake
  }

  view fn get_protocol_fee_bps(): int {
    return self.protocol_fee_bps
  }

  // ============================================================
  // Governance
  // ============================================================

  fn set_params(
    new_min_session_deposit: int,
    new_min_tailnet_deposit: int,
    new_session_grace_epochs: int,
    new_sweep_grace_multiplier: int,
    new_sweep_bounty_bps: int,
    new_min_endpoint_stake: int,
    new_unbond_grace_epochs: int,
    new_slash_burn_bps: int,
    new_slash_bounty_bps: int,
    new_protocol_fee_bps: int
  ): bool {
    require_owner()
    require(new_min_session_deposit > 0, "session deposit > 0")
    require(new_min_tailnet_deposit > 0, "tailnet deposit > 0")
    require(new_session_grace_epochs > 0, "session grace > 0")
    require(new_sweep_grace_multiplier > 0, "sweep > 0")
    require(new_sweep_bounty_bps <= 1000, "sweep bounty <= 10%")
    require(new_min_endpoint_stake >= 100000000, "stake floor too low")
    require(new_unbond_grace_epochs >= 1000, "unbond grace too short")
    require(new_slash_burn_bps >= 5000, "burn share too low")
    require(new_slash_burn_bps + new_slash_bounty_bps == BPS_DENOM, "slash bps sum")
    require(new_protocol_fee_bps <= 200, "protocol fee > 2%")
    self.min_session_deposit = new_min_session_deposit
    self.min_tailnet_deposit = new_min_tailnet_deposit
    self.session_grace_epochs = new_session_grace_epochs
    self.sweep_grace_multiplier = new_sweep_grace_multiplier
    self.sweep_bounty_bps = new_sweep_bounty_bps
    self.min_endpoint_stake = new_min_endpoint_stake
    self.unbond_grace_epochs = new_unbond_grace_epochs
    self.slash_burn_bps = new_slash_burn_bps
    self.slash_bounty_bps = new_slash_bounty_bps
    self.protocol_fee_bps = new_protocol_fee_bps
    return true
  }

  fn set_paused(v: int): bool {
    require_owner()
    self.paused = v
    return true
  }

  fn transfer_ownership(new_owner: address): bool {
    require_owner()
    require(is_address(new_owner), "bad addr")
    self.owner = new_owner
    return true
  }

  fn withdraw_program_treasury(to: address, amount: int): bool {
    require_owner()
    require(amount > 0, "amount > 0")
    require(self.treasury >= amount, "treasury insufficient")
    self.treasury = self.treasury - amount
    require(transfer(to, amount), "transfer failed")
    emit ProgramTreasuryWithdrawn(to, amount)
    return true
  }
}