# 集成提供者

在你的应用中运行 URnetwork 提供者。每个安装都作为你网络中的客户端共享连接，收益进入你只需设定一次的同一个 Bittensor 钱包。

![提供者集成 — 你的后端为用户设备上的你的应用开通每个提供者安装，应用通过 URnetwork 向客户端共享连接，而 coldkey 持有者的网络同意（离线签署一次、由你的后端提交）把子网收益导向固定的收款 coldkey；任何密钥都不会随应用分发](/docs-assets/diagram-dev-integrate-provider.svg)

提供者共享设备的互联网连接：其他 URnetwork 用户的流量经由它出网。运行期间，你的应用显示四项内容：是否正在共享连接、服务过的客户端、提供的数据量，以及只读的收款钱包。收益总额来自你的后端。可运行的示例覆盖十二种语言和三个独立平台：Electron、Tauri 和 Android。它们都实现了[提供者契约](https://github.com/urnetwork/examples/blob/main/PROVIDER_CONTRACT.md)，确切的规则都在其中；本页解释各个部分如何配合。

## 谁持有什么

提供者集成把工作分给三方，任何密钥都不会随应用分发。

- **你的后端**持有你网络的根 JWT。它把每个安装开通为提供者安装（provider install），把受限的客户端 JWT 交付给每个安装，并请求钱包同意消息。
- **coldkey 持有者**用自己的钱包工具离线签署每一份钱包同意，例如 btcli、polkadot.js 扩展或 subkey。coldkey 私钥永远不会到达你的后端或应用。
- **应用**用自己的客户端 JWT 运行 SDK 的本地 Device，公开共享连接并显示状态。它从不持有根 JWT，也从不签署或提交同意。

| 值 | 保存在 | 用途 |
| --- | --- | --- |
| `URNETWORK_ROOT_JWT` | 你后端的密钥存储 | 开通提供者安装，并把它们映射到收款钱包 |
| 收款 coldkey 地址（ss58） | 你后端的配置 | 固定的收款钱包：公开，但只由后端设定 |
| coldkey 私钥 | 其持有者，离线保存 | 签署钱包同意 |
| `client.jwt` | 安装的私有状态 | 该安装的受限凭证，由 SDK 刷新 |

收款以 Bittensor 子网收益的形式支付给 coldkey。应用读取收款钱包只为显示，自身没有任何收款设置。

## 先取得同意

共享连接时，其他用户的流量从用户的连接出网，所以你的应用必须在开始前征得同意，并允许用户随时停止。每个示例都原样显示这段免责声明，契约固定了它的 SHA-256：

```text
Consent disclaimer: an app that integrates a URnetwork provider must collect the user's consent before it provides.
Providing shares the user's internet connection: other URnetwork users' traffic exits through the user's device and IP address.
This example starts providing without asking, because the consent screen belongs to your app.
```

控制台示例在启动时打印它。GUI 应用和 Android 应用把它显示在开始共享的控件旁边。示例本身没有同意界面，因为那个界面属于你的应用；它们也不限制带宽。

## 开通提供者安装

每个安装都是一个**提供者安装**（provider install）：你网络中只共享连接的顶层客户端。你的后端用根 JWT 调用 `POST /network/auth-client` 来创建它，发送描述、设备规格和 `provide_intent` 标志，但不带 `client_id` 或 `source_client_id`：

```json
{
  "description": "provider user:alice:laptop-1",
  "device_spec": "urnetwork-examples/go-provider",
  "provide_intent": true
}
```

响应带有新的 `client_id` 及其受限的 `by_client_jwt`，后端把它作为 `client.jwt` 交付给该安装。服务器在客户端的整个生命周期内保存这个标志：

- 提供者安装永远不会出现在你网络的对等节点列表中（`GET /network/peers` 和对等节点变更流），也永远收不到这份列表。
- 它不计入 100 个顶层客户端的上限，也不计入维持对等节点列表开启的 100 个近期活跃顶层客户端。
- 它的创建不受该上限和你套餐的并发客户端检查约束。

这个标志不会让设备开始共享。只要设备在公开共享，SDK 就会自行在自己的连接上声明提供意图（provide intent），下文的客户端上限规则适用于声明了它的连接。应用无需为此设置任何东西。没有公开共享就连接的提供者安装，按普通客户端计算。

要重新签发令牌，就发送保存的 `client_id` 以及描述和设备规格。重新签发时服务器会忽略 `provide_intent`，所以只在创建时发送这个标志。连续 30 天没有连接的客户端会被停用：它的重新签发请求得到 `Client does not exist.`，此时后端应开通一个新的提供者安装。

有两类客户端不是提供者安装。个人自己的、同时也在共享的设备会自行声明意图，并保持为网络的对等节点，所以不要为它设置这个标志。用 `source_client_id` 开通的子客户端永远不会作为提供者提供给其他用户，也赚不到任何收益。

[Go 后端工具](https://github.com/urnetwork/examples/blob/main/go/provider/README.md#backend-provision-and-map-the-payout-wallet)用 `wallet provision <installation-key> <client-jwt-file>` 完成以上全部工作，[契约](https://github.com/urnetwork/examples/blob/main/PROVIDER_CONTRACT.md#backend-provision-provider-clients)给出了同样请求的 curl 写法。

## 提供者与客户端上限

提供者在取得资格的过程中以及取得资格之后，都不受你套餐的并发客户端上限约束。提供者取得资格需要三项条件：

- 公开共享模式，并已注册其提供密钥（provide keys）；
- 通过一次出口健康探测（egress-health）；
- 有了 12 小时的历史之后，12 小时可靠性至少为 0.7。

新的提供者有 1 小时的宽限期来取得资格，而且在至少尝试过一次出口探测之前，宽限期永远不会结束。取得资格的提供者每 4 小时重新检查一次。每个客户端每 8 小时有一次资格尝试。

未取得资格的提供者在有空闲名额时占用一个普通客户端名额。没有空闲名额时，平台可能会断开它。随后 SDK 会让该客户端与平台的所有连接暂停 15 分钟，外加最多 5 分钟的随机抖动（jitter），然后自行重新连接。在此期间，应用显示 `client limit, retry at HH:MM UTC`：以 UTC 表示的重试时间，向上取整到整分钟。除此之外，平台不会告诉客户端任何关于资格的信息。

## 映射收款钱包

收款钱包是你固定的 Bittensor coldkey，以 ss58 地址表示。服务器只通过 coldkey 持有者签署的钱包映射同意向提供者客户端付款。同意分两种：

| 同意 | 一次签名覆盖 | 适用于 |
| --- | --- | --- |
| 网络同意 | 你网络中现在和将来的所有提供者客户端 | 嵌入提供者的应用：持有者每个网络只签一次 |
| 单个提供者同意 | 一个提供者客户端 | 收益进入另一个 coldkey 的客户端，例如由独立签名者运营的客户端 |

对每个提供者客户端的每个纪元，生效中的单个提供者同意优先；否则由网络同意付款。一份同意覆盖从它的 `from_epoch` 到 `through_epoch` 的纪元，最多 65,536 个，并且只向前生效。要更换 coldkey，就签一份同类的新同意；之前的纪元保留之前的 coldkey。不存在不经签名的撤销。

每份同意需要四步，只有签名来自你的后端之外：

1. 从 `GET /sn/epoch` 读取当前纪元 `E`。
2. 用根 JWT 请求消息：`POST /sn/wallet/network-consent`，带上 coldkey 和从 `E+1` 到 `E+65536` 的纪元；或者 `POST /sn/wallet/consent`，另外带上 `client_id`。
3. coldkey 持有者在五分钟内签署消息的确切字节。
4. 把 coldkey、消息和签名提交到 `POST /sn/wallet`，单个提供者同意还要带上同一个 `client_id`。用 `GET /sn/wallet` 检查映射。

签名是 `substrate` 签名上下文中针对消息 UTF-8 字节的 sr25519 签名，可以是原始字节，也可以包装成 `<Bytes>…</Bytes>`。包装形式就是 Polkadot 扩展的 `signRaw`（类型为 `bytes`）所签的内容。签名为 64 字节，以十六进制发送。网络同意消息以 `Approve URnetwork network wallet mapping` 开头，单个提供者同意消息以 `Approve URnetwork provider wallet mapping` 开头。

[Go 钱包工具](https://github.com/urnetwork/examples/blob/main/go/provider/README.md#backend-provision-and-map-the-payout-wallet)两种都支持：`network-challenge` 和 `network-accept` 用于网络同意，`challenge` 和 `accept` 用于单个客户端，`show` 用于列出它们。不支持网络同意的服务器对 `POST /sn/wallet/network-consent` 返回 404；单个提供者同意在那里仍然有效。第三种更低一级的模式，即 hotkey 委托，将随后续的服务器和 SDK 变更推出。应用只需把它标记为 `(hotkey)`。

## 应用内部

每个安装把状态保存在一个私有目录中：`client.jwt`、`instance-id`、`identity.json`，以及 SDK 有大小上限的 `logs/`。控制台示例从 `URNETWORK_PROVIDER_STATE_DIR` 读取该目录的绝对路径；GUI 应用和 Android 应用使用各自的私有应用数据。在 POSIX 上，该目录权限为 0700，其中的文件为 0600，每个文件都以原子方式替换。`identity.json` 让提供者的密钥在重启后保持不变。

应用的生命周期很短：

1. 显示同意免责声明，并加载安装状态。
2. 用 SDK 支持扩展器的构造函数和安装的密钥材料创建 Device。首次运行时还没有密钥材料：Device 会生成新的身份，应用将其保存为 `identity.json`。
3. 把每个刷新后的令牌保存为 `client.jwt`，并在登出时停止。
4. 把共享模式设为公开。之后 SDK 会自行声明提供意图。
5. 每秒读取一次状态，并在启动时以及每 10 分钟读取一次收款钱包。

每个示例都按相同的规则显示相同的四个字段：

| 字段 | 显示内容 |
| --- | --- |
| 状态 | `stopped`、`client limit`、`paused`、`providing` 或 `starting`，按此顺序判断 |
| 服务过的客户端 | 自应用启动以来，与该提供者建立过合同的不同客户端 |
| 提供的数据 | 双向为客户端转发的字节数，使用二进制单位 |
| 收款钱包 | coldkey 及其范围：`(this provider)`、`(network)`、`(hotkey)` 或 `(another provider)` |

每当状态、服务过的客户端数或收款钱包发生变化，控制台示例就打印一行状态，否则每分钟打印一次：

```text
status: providing | clients served: 3 | data provided: 12.4 MiB | payout wallet: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY (network)
```

`providing` 要求共享已启用并已连接到平台。钱包在首次读取前显示 `checking`，首次读取失败时显示 `unavailable`，没有映射钱包时显示 `not set`。上面的地址是公开的 Substrate 开发账户，仅用作示例数据。[契约中的标准向量](https://github.com/urnetwork/examples/blob/main/PROVIDER_CONTRACT.md#status)精确规定了每一段文本。

## 扩展器角色

在共享连接期间，Device 还会运行提供者的扩展器角色，该角色默认开启。它监听 TCP 443 以及 UDP 443 和 4053，让无法直接连到平台的客户端可以经由这个提供者连接。只有子网的命令行提供者（`provider provide`）还会监听 UDP 53，客户端则会同时尝试 UDP 53 和 4053。Windows 和 macOS 可能会在第一次时询问用户是否允许传入连接。在 Linux 上，没有权限绑定这些端口的进程会在不运行该角色的情况下继续共享。SDK 的 Android 和 iOS 构建不包含这个角色。

只需一项设置即可关闭这个默认值：把 `DefaultProvideExtender` 设为 false。

| 绑定 | 默认关闭 |
| --- | --- |
| Go | `DeviceLocalSettings.DefaultProvideExtender = false`，或把构造函数的最后一个参数设为 `false` |
| gomobile | `Sdk.newDeviceLocalWithProvideExtender`（Java、Kotlin）或 `SdkNewDeviceLocalWithProvideExtender`（Swift），默认值设为 false |
| C ABI | `urnet_new_device_local_with_provide_extender`，`default_provide_extender` 设为 false |
| C++ | `urnet::newDeviceLocalWithProvideExtender`，默认值设为 false |

用户自己通过 `SetProvideExtender` 做出的选择仍然优先于默认值。`ProvideExtenderEnabled` 设为 false 则是总开关：该角色永远不会运行。这两项设置以及接收它们的构造函数，从 sdk `c638dfa8` 之后的第一个发布版起出现在 SDK 中。

## 在后台运行

控制台示例共用三份[后台运行模板](https://github.com/urnetwork/examples/blob/main/background/README.md)：Linux 上的 systemd 用户服务、macOS 上的 launchd 代理，以及 Windows 上的计划任务。每份模板都指定安装的状态目录，所以服务定义里只有路径，从不包含凭证。所有示例都使用相同的退出码：

| 退出码 | 含义 |
| --- | --- |
| 0 | 请求的停止，例如 Ctrl-C 或 SIGTERM |
| 78 | 重启也无法修复的配置或凭证问题 |
| 1 | 其他任何失败 |

监管程序会在失败后重启提供者，但在 0 或 78 之后不会。launchd 没有按退出码区分的规则：在 78 之后，它会每分钟重试一次，直到状态被修复。

GUI 应用和 Android 应用会自行持续共享。Electron 和 Tauri 在窗口关闭后留在系统托盘中，并提供登录时启动。Android 在用户启动的前台服务中共享连接，并在按流量计费的网络上暂停共享。

## 示例

每个示例的 README 都包含其所支持的每个操作系统的构建和运行步骤，以及无需凭证的自检。控制台示例可在 Windows、macOS 和 Linux 上运行。[Go 提供者](https://github.com/urnetwork/examples/blob/main/go/provider/README.md)是参考实现。

| 平台 | 示例 | 构建基础 |
| --- | --- | --- |
| C | [C 提供者](https://github.com/urnetwork/examples/blob/main/c/provider/README.md) | C ABI |
| C++ | [C++ 提供者](https://github.com/urnetwork/examples/blob/main/cpp/provider/README.md) | C ABI |
| C# | [C# 提供者](https://github.com/urnetwork/examples/blob/main/csharp/provider/README.md) | 通过 .NET 使用 C ABI |
| Go | [Go 提供者](https://github.com/urnetwork/examples/blob/main/go/provider/README.md) | Go SDK |
| Java | [Java 提供者](https://github.com/urnetwork/examples/blob/main/java/provider/README.md) | 通过 JNA 使用 C ABI |
| JavaScript | [JavaScript 提供者](https://github.com/urnetwork/examples/blob/main/javascript/provider/README.md) | Node 与原生 companion |
| Kotlin | [Kotlin 提供者](https://github.com/urnetwork/examples/blob/main/kotlin/provider/README.md) | 通过 JNA 使用 C ABI |
| Python | [Python 提供者](https://github.com/urnetwork/examples/blob/main/python/provider/README.md) | 通过 ctypes 使用 C ABI |
| Ruby | [Ruby 提供者](https://github.com/urnetwork/examples/blob/main/ruby/provider/README.md) | 通过 FFI 使用 C ABI |
| Rust | [Rust 提供者](https://github.com/urnetwork/examples/blob/main/rust/provider/README.md) | 通过 Rust crate 使用 C ABI |
| Swift | [Swift 提供者](https://github.com/urnetwork/examples/blob/main/swift/provider/README.md) | 通过 SwiftPM C 模块使用 C ABI |
| TypeScript | [TypeScript 提供者](https://github.com/urnetwork/examples/blob/main/typescript/provider/README.md) | Node 与原生 companion |
| Electron | [Electron 提供者](https://github.com/urnetwork/examples/blob/main/electron/provider/README.md) | 基于原生 companion 的桌面应用 |
| Tauri | [Tauri 提供者](https://github.com/urnetwork/examples/blob/main/tauri/provider/README.md) | 在同一进程中使用 Rust crate 的桌面应用 |
| Android | [Android 提供者](https://github.com/urnetwork/examples/blob/main/android/provider/README.md) | 在前台服务中运行的 gomobile AAR |

**发布状态：**提供者示例需要一个包含提供者 API 的 SDK 发布版：支持扩展器的构造函数、客户端上限状态，以及钱包同意的范围。它们从 sdk `c638dfa8` 之后的第一个发布版起出现在 SDK 中。[安装 SDK](/docs/install-sdk) 介绍各个包，[示例浏览页](/docs/examples)收录每种语言的其他示例。
