> ## Documentation Index
> Fetch the complete documentation index at: https://cobo.com/developers/llms.txt
> Use this file to discover all available pages before exploring further.

# TSS Node 事件通知

> 配置 TSS Node 将密钥生成、签名和重分片等事件推送到您自己的服务器，并使用节点的 RSA 公钥验签。

<Tip>
  即刻安装 [Cobo WaaS Skill](/developers/v2_cn/guides/overview/cobo-waas-skill)，在 Claude Code、Cursor 等 AI 开发环境中使用自然语言集成 WaaS API，显著提升开发效率 🚀
</Tip>

TSS Node 在完成密钥生成（KeyGen）、密钥重分片（KeyReshare）、签名（KeySign）或分片签名（KeyShareSign）等操作时，可以将事件通知推送到您自己的服务器。节点以 HTTP POST 请求的形式发送到您配置的地址。

每条消息通过 JSON Web Token（JWT）承载，并使用 RS256 签名。您的接收服务器使用节点的 RSA 公钥验签，从而确认消息来自您的节点。

<Note>
  这些事件通知与 Cobo Webhook 事件不是同一套机制。Cobo Webhook 事件（`wallets.mpc.tss_request.*`）由 Cobo 平台发送到您在 Cobo Portal 注册的地址，并使用 Cobo 的 Webhook 签名。本页描述的事件由您自己部署的 TSS Node 发送，使用 `request.*` 事件名，由节点的 RSA 密钥签名，并在节点配置文件中配置。订阅其中一套不会收到另一套。关于 Cobo Webhook 事件，请参考 [Webhook 事件类型](/developers/v2_cn/guides/webhooks-callbacks/webhook-event-type)。
</Note>

## 开始之前

事件通知使用一对 RSA 2048 密钥。请在首次启动节点前生成密钥对并存入节点数据库：

```bash theme={null}
# 生成密钥对并存入数据库
./tss-node event key create

# 查看公钥，供接收服务器验签使用
./tss-node event key info
```

公钥以 PEM 格式输出，请将其复制到接收服务器。

## 配置事件通知

在 TSS Node 配置文件（如 `cobo-tss-node-config.yaml`）中添加或取消注释 `event` 配置段：

```yaml theme={null}
event:
  server:
    - url: http://your-event-server:11030/v2/event
  event_types:
    - request.keygen.succeeded
    - request.keyreshare.succeeded
    - request.keysign.succeeded
    - request.keysharesign.succeeded
  token_expire_minutes: 2
  retry_times: 60
  sleep_seconds: 60
  request_timeout: 10
  monitor_interval: 10s
```

### 顶层参数

| 参数                     | 类型    | 默认值        | 说明                                       |
| ---------------------- | ----- | ---------- | ---------------------------------------- |
| `server`               | 列表    | 无          | 接收服务器列表，可配置多个。                           |
| `event_types`          | 字符串列表 | `[]`，即不推送  | 订阅的事件类型。各服务器未单独配置时继承此列表。                 |
| `token_expire_minutes` | 整数    | `2`        | JWT 有效期（分钟）。超过该时间后，您的服务器应拒绝该请求。          |
| `retry_times`          | 整数    | `60`       | 推送失败后的最大重试次数。`0` 表示无限重试。                 |
| `sleep_seconds`        | 整数    | `60`       | 两次重试之间的等待秒数。间隔固定，不采用指数退避。                |
| `request_timeout`      | 整数    | `10`       | 单次 HTTP 请求的超时时间（秒）。                      |
| `proxy`                | 字符串   | 空          | 可选的 HTTP 代理地址，如 `http://proxy:8080`。     |
| `monitor_interval`     | 字符串   | 空，即不启用健康检查 | 健康检查间隔，采用 Go duration 格式，如 `10s` 或 `1m`。 |

### 服务器级参数

`server` 下的每个条目都继承顶层配置，并可覆盖其中任意一项。

| 参数                     | 类型    | 说明                                 |
| ---------------------- | ----- | ---------------------------------- |
| `url`                  | 字符串   | 必填。事件推送的目标 URL。                    |
| `event_types`          | 字符串列表 | 该服务器订阅的事件类型。省略时继承顶层 `event_types`。 |
| `token_expire_minutes` | 整数    | 覆盖顶层配置。                            |
| `retry_times`          | 整数    | 覆盖顶层配置。                            |
| `sleep_seconds`        | 整数    | 覆盖顶层配置。                            |
| `request_timeout`      | 整数    | 覆盖顶层配置。                            |
| `proxy`                | 字符串   | 覆盖顶层配置。                            |
| `monitor_interval`     | 字符串   | 覆盖顶层配置。                            |

以下示例将签名事件推送到一台服务器，并将全部事件推送到审计服务器，同时为审计服务器配置更长的重试预算：

```yaml theme={null}
event:
  token_expire_minutes: 2
  retry_times: 60
  sleep_seconds: 30
  request_timeout: 10
  monitor_interval: 30s
  server:
    - url: http://primary-server:11030/v2/event
      event_types:
        - request.keysign.succeeded
        - request.keysign.failed
    - url: http://audit-server:11031/v2/event
      event_types:
        - request.keygen.succeeded
        - request.keyreshare.succeeded
        - request.keysign.succeeded
        - request.keysharesign.succeeded
      retry_times: 120
      sleep_seconds: 60
```

同一事件可以推送给多台服务器。每台服务器独立维护重试状态，某一台失败不影响其他服务器的投递。

## 支持的事件类型

请只订阅您的集成会实际处理的事件类型，避免节点在您并不使用的事件上消耗投递次数。

### KeyGen

| 事件类型                       | 触发时机           |
| -------------------------- | -------------- |
| `request.keygen.created`   | 密钥生成请求已创建。     |
| `request.keygen.updated`   | 密钥生成过程中状态发生变更。 |
| `request.keygen.succeeded` | 密钥生成成功完成。      |
| `request.keygen.failed`    | 密钥生成失败。        |

### KeySign

| 事件类型                        | 触发时机         |
| --------------------------- | ------------ |
| `request.keysign.created`   | 签名请求已创建。     |
| `request.keysign.updated`   | 签名过程中状态发生变更。 |
| `request.keysign.succeeded` | 签名成功完成。      |
| `request.keysign.failed`    | 签名失败。        |

### KeyReshare

| 事件类型                           | 触发时机          |
| ------------------------------ | ------------- |
| `request.keyreshare.created`   | 密钥重分片请求已创建。   |
| `request.keyreshare.updated`   | 重分片过程中状态发生变更。 |
| `request.keyreshare.succeeded` | 密钥重分片成功完成。    |
| `request.keyreshare.failed`    | 密钥重分片失败。      |

### KeyShareSign

| 事件类型                             | 触发时机           |
| -------------------------------- | -------------- |
| `request.keysharesign.created`   | 分片签名请求已创建。     |
| `request.keysharesign.updated`   | 分片签名过程中状态发生变更。 |
| `request.keysharesign.succeeded` | 分片签名成功完成。      |
| `request.keysharesign.failed`    | 分片签名失败。        |

## 请求格式

节点以表单编码的 POST 请求发送事件，JWT 放在 `TSS_JWT_MSG` 字段中：

```http theme={null}
POST /v2/event HTTP/1.1
Content-Type: application/x-www-form-urlencoded

TSS_JWT_MSG=<JWT 字符串>
```

JWT 使用 RS256 签名，签名私钥为节点本地存储的 RSA 2048 私钥。

JWT Header：

```json theme={null}
{
  "alg": "RS256",
  "typ": "JWT"
}
```

JWT Payload：

```json theme={null}
{
  "package_data": "<Base64 编码的事件 JSON>",
  "exp": 1700000000,
  "iss": ""
}
```

| Claim          | 说明                                                |
| -------------- | ------------------------------------------------- |
| `package_data` | 事件体先序列化为 JSON，再进行 Base64 编码。                      |
| `exp`          | 过期时间，Unix 时间戳（秒），等于发送时间加上 `token_expire_minutes`。 |
| `iss`          | 固定为空字符串。                                          |

您的服务器必须返回 `200 OK` 或 `201 Created`。节点将其他状态码一律视为失败并重试。

## 事件结构

解码 `package_data` 后，事件体结构如下：

```json theme={null}
{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "request.keysign.succeeded",
  "node_id": "your-node-id",
  "created_timestamp": 1700000000000,
  "data": { }
}
```

| 字段                  | 类型    | 说明                                  |
| ------------------- | ----- | ----------------------------------- |
| `event_id`          | 字符串   | 事件唯一 ID（UUID）。同一事件可能重复到达，请用它实现幂等处理。 |
| `event_type`        | 字符串   | 事件类型，如 `request.keysign.succeeded`。 |
| `node_id`           | 字符串   | 产生该事件的节点 ID。                        |
| `created_timestamp` | int64 | 事件创建时间，Unix 时间戳（毫秒）。                |
| `data`              | 对象    | 事件详情，结构随 `event_type` 不同而不同，见下文。    |

每个 `data` 对象都包含 `data_type`、`request_id`、`request_type`、`request_status`、`request_detail`、`extra_info`、`failed_reason` 和 `result`，其中 `request_detail` 与 `result` 内部的字段按操作类型不同。

<CodeGroup>
  ```json KeyGen theme={null}
  {
    "data_type": "KeyGen",
    "request_id": "req-xxx",
    "request_type": "KeyGen",
    "request_status": 3,
    "request_detail": {
      "threshold": 2,
      "curve": 0,
      "node_ids": ["node1", "node2", "node3"],
      "task_id": "task-xxx",
      "biz_task_id": "biz-xxx"
    },
    "extra_info": "",
    "failed_reason": "",
    "result": {
      "group_id": "group-xxx",
      "root_pub_key": "04abcd..."
    }
  }
  ```

  ```json KeySign theme={null}
  {
    "data_type": "KeySign",
    "request_id": "req-xxx",
    "request_type": "KeySign",
    "request_status": 3,
    "request_detail": {
      "group_id": "group-xxx",
      "root_pub_key": "04abcd...",
      "used_node_ids": ["node1", "node2"],
      "bip32_path_list": ["m/44/0/0/0/0"],
      "msg_hash_list": ["abcdef..."],
      "tweak_list": [],
      "signature_type": "ecdsa",
      "tss_protocol": "gg20",
      "task_id": "task-xxx",
      "biz_task_id": "biz-xxx"
    },
    "extra_info": "",
    "failed_reason": "",
    "result": {
      "signatures": ["sig-hex..."]
    }
  }
  ```

  ```json KeyReshare theme={null}
  {
    "data_type": "KeyReshare",
    "request_id": "req-xxx",
    "request_type": "KeyReshare",
    "request_status": 3,
    "request_detail": {
      "old_group_id": "old-group-xxx",
      "root_pub_key": "04abcd...",
      "curve": 0,
      "used_node_ids": ["node1", "node2"],
      "old_threshold": 2,
      "new_threshold": 3,
      "new_node_ids": ["node1", "node2", "node3", "node4"],
      "task_id": "task-xxx",
      "biz_task_id": "biz-xxx"
    },
    "extra_info": "",
    "failed_reason": "",
    "result": {
      "group_id": "new-group-xxx",
      "root_pub_key": "04abcd..."
    }
  }
  ```

  ```json KeyShareSign theme={null}
  {
    "data_type": "KeyShareSign",
    "request_id": "req-xxx",
    "request_type": "KeyShareSign",
    "request_status": 3,
    "request_detail": {
      "group_id": "group-xxx",
      "used_node_ids": ["node1", "node2"],
      "bip32_path_list": ["m/44/0/0/0/0"],
      "msg_hash_list": ["abcdef..."],
      "task_id": "task-xxx",
      "biz_task_id": "biz-xxx"
    },
    "extra_info": "",
    "failed_reason": "",
    "result": { }
  }
  ```
</CodeGroup>

## 投递与重试

某次投递失败后，节点等待 `sleep_seconds` 再重试，最多重试 `retry_times` 次。间隔固定，不会随重试次数增长。

以下行为对每台已配置的服务器都适用：

* 将 `retry_times` 设为 `0` 表示无限重试，直到推送成功或节点停止。
* 节点在首次尝试前将事件写入数据库，状态为 `pending`。节点重启后会恢复投递，最多恢复 1000 条待推送事件。
* 推送成功后节点删除该事件记录；重试次数用尽后将该事件标记为 `failed`。

由于失败会重试，您的服务器可能重复收到同一事件，请按 `event_id` 去重。

下表说明两个重试参数如何配合：

| 目标         | `retry_times` | `sleep_seconds` | 效果            |
| ---------- | ------------- | --------------- | ------------- |
| 不丢失关键事件    | `0`           | `30`            | 以 30 秒间隔无限重试。 |
| 兼顾可靠性与队列吞吐 | `60`          | `60`            | 最多重试约一小时。     |
| 尽快暴露投递问题   | `3`           | `5`             | 尽早放弃，避免堵塞队列。  |

## 健康检查

当 `monitor_interval` 非空时，节点会定期向每台事件服务器发送 ping 请求：

```yaml theme={null}
monitor_interval: 30s
```

ping 请求使用与事件相同的 JWT 机制，其 `package_data` 中的 `event_type` 为 `ping`。ping 失败时节点最多重试 2 次，间隔 3 秒。

请将 `ping` 与业务事件分开处理：返回 `200 OK` 即可，无需其他处理。

## 实现接收服务器

Cobo 提供 Go 与 Java 两个事件服务器模板，您可以克隆后填入自己的业务逻辑：

* 仓库地址：[cobo-mpc-callback-server-v2-template](https://github.com/CoboGlobal/cobo-mpc-callback-server-v2-template)
* Go 实现：`cobo-mpc-event-server-golang/`
* Java 实现：`cobo-mpc-event-server-java/`

两个模板的处理流程一致：

1. 监听 `POST /v2/event`，从 `TSS_JWT_MSG` 表单字段中读取 JWT。
2. 使用节点的 RSA 公钥（存放于 `configs/tss-node-event-pub.key`）验签，验签失败时返回 `400 Bad Request`。
3. 从 `package_data` claim 中解码出事件 JSON。
4. 使用 `cobo-waas2` SDK 反序列化为 `TSSEvent`，再按事件类型分发处理。
5. 处理成功返回 `200 OK`。`ping` 事件直接返回 `200 OK`，无需其他处理。

具体实现细节请参考仓库中各模板的 `README.md` 与源码。

## 推荐配置

以下配置可避免关键事件被丢弃，并将 `token_expire_minutes` 设得高于 `request_timeout`，使 token 不会在请求超时之前先过期：

```yaml theme={null}
event:
  server:
    - url: https://your-event-receiver/v2/event
  event_types:
    - request.keygen.succeeded
    - request.keygen.failed
    - request.keysign.succeeded
    - request.keysign.failed
    - request.keyreshare.succeeded
    - request.keyreshare.failed
    - request.keysharesign.succeeded
    - request.keysharesign.failed
  token_expire_minutes: 5
  retry_times: 0
  sleep_seconds: 30
  request_timeout: 10
  monitor_interval: 60s
```

节点在每次重试时都会重新生成 JWT，因此当 `sleep_seconds` 较大时，请相应提高 `token_expire_minutes`。

## 常见问题

<AccordionGroup>
  <Accordion title="如何获取用于验签的公钥？">
    在节点上运行 `./tss-node event key info`，该命令以 PEM 格式输出公钥。将该公钥配置到接收服务器。
  </Accordion>

  <Accordion title="同一事件会被重复投递吗？">
    会。网络失败，或您的服务器未返回 `200 OK` 或 `201 Created` 时，节点会重试。请按 `event_id`（UUID）去重。
  </Accordion>

  <Accordion title="节点重启后未投递成功的事件会丢失吗？">
    不会。节点在首次投递前会将事件以 `pending` 状态写入数据库，重启后恢复投递。
  </Accordion>

  <Accordion title="retry_times 设为 0 与不配置有何区别？">
    设为 `0` 表示无限重试；不配置则使用默认值 `60`。
  </Accordion>

  <Accordion title="token_expire_minutes 应该设为多少？">
    建议设为 2 到 5 分钟，并保持高于 `request_timeout`，使 token 不会在请求超时之前先过期。当 `sleep_seconds` 较大时应进一步提高，因为节点在每次重试时都会重新生成 JWT。
  </Accordion>

  <Accordion title="可以配置多台接收服务器吗？">
    可以。在 `server` 下为每台服务器添加一个条目，各服务器独立接收事件、独立重试。
  </Accordion>
</AccordionGroup>
