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

# 函数代码

> 用户函数在 BlockX 里的形态：入口约定、参数与返回值、inline 与注册两种来源、按 task 固定的代码版本、函数里能用的 SDK，以及静态审计。

用户函数是一段 Python 源码。它在 Worker 本机的 Python Executor 进程里被编译、缓存和调用：每个 call 调用一次入口函数，入口函数的返回值就是这个 call 的输出。函数只负责"算"，不负责"写"。

## 入口约定

Executor 按下面的优先级在源码的顶层 `def` 里选入口（`python/blockx_audit/tables.py` 的 `resolve_entry_name`）：

1. `functionId` 的最后一段。例如注册函数 `token.trace_to_token_transfer` 的入口是 `def trace_to_token_transfer`。
2. 名为 `_` 的顶层 `def`，或者一行 `_ = <函数名>` 别名。blockx-py 把 Python callable 打包成源码时会自动补这行别名。
3. 源码顺序里第一个不以 `_` 开头的顶层 `def`。

inline 源码最简单的写法是直接定义 `_`：

```python theme={null}
def _(chain_id, record):
    value = float(record.get("value") or 0)
    if value <= 0 or not record.get("tx_id"):
        return None
    return {
        "id": record["id"],
        "chain_id": chain_id,
        "from_addr": record["from_addr"],
        "to_addr": record["to_addr"],
        "value": value,
    }
```

函数体要自包含：模块级常量和辅助函数要写进函数体，用到的模块在函数体内 `import`。

## 参数与返回值

参数按位置传入，来自 call 的 `args`：

| Call Builder                                     | 参数从哪来                                                                     |
| ------------------------------------------------ | ------------------------------------------------------------------------- |
| `InputsCallConfig`                               | `callList` 的每一行就是一次调用的参数数组                                                |
| `BlockTableCallConfig` / `BlockBundleCallConfig` | 按 `params` 模板拼装：`"${表名}"` 的位置换成触发表的一行（dict），其余位置原样传入；`params` 为空时整行作为唯一参数 |

行 dict 的键是源表的列名，值是 JSON 解码后的值。blockx-py 里用 `SOURCE_ROW` 常量代替手写 `"${表名}"`。

返回值必须能被 JSON 序列化：

| 返回           | 含义                                             |
| ------------ | ---------------------------------------------- |
| `dict`       | 一行。写表插件只投影其中属于目标表的列，多余的键被忽略                    |
| `list[dict]` | 多行                                             |
| `None`       | 这次调用不产出。写表插件跳过它；`ReturnValueHandler` 记为 `null` |

## 函数从哪里来

| 来源        | 声明方式                                                                               | 身份                                            |
| --------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
| inline 源码 | 随 task 提交：`code.sourceCode`；blockx-py 里用 `Function(source_code=...)` 或直接传 callable | Worker 按源码 SHA-256 生成 `inline-<digest 前 8 位>` |
| 注册函数      | 只给 `functionId`（形如 `space.name`），源码来自函数表                                           | `functionId` 加上快照里的 digest                    |

注册函数的源码由 Worker 的 Function Code View 维护：订阅 BlockDB 的 `system.function` 表，或按周期从 Redis hash 全量刷新（`FUNCTION_CODE_REDIS_URL`），每次变更发布一份完整的新快照。本地开发时用内存函数库（devstub）。

## 版本固定

* task 激活时固定一个快照 `epoch`；这个 task 的所有 call 和子调用都按同一份快照解析函数，热更新只影响之后提交的 task。
* 派发给 Executor 的只有源码 digest。Executor 按 digest 缓存编译产物，未命中时按 digest 向 Worker 回拉源码。
* 同一 task 内，把同一个 `functionId` 绑到不同源码，或 inline 函数与快照函数撞名，都会让这个 ID 确定性失败。

## 函数里能做什么

| 能力           | 写法                                                                 | 发生了什么                                                                                            |
| ------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| 读 BlockDB    | blockdb-py 的 `Table` / `BlockTable` / `TimeTable`                  | gRPC channel 在 Executor 启动时被替换成 BridgeChannel，请求经 UDS 回到 Worker 的 IO 子系统，享受缓存、singleflight 和准入控制 |
| 调链节点         | leafage-py（`ChainState` 等）                                         | 同样被 patch 回 Worker 的 NodeRPC 适配器                                                                 |
| 调用其他函数       | `from blockx import function`，`function.call("space.name", *args)` | 在 Executor 内变成子调用：同一 task tree、同一代码快照、同一 IO scope；深度上限 `MaxSubcallDepth = 8`                     |
| 能力后端         | blockx-py 里的 `LocalTestService`、`Router` 等类                        | 普通 gRPC client，被拦截后在 Worker 进程内分派                                                                |
| `time.sleep` | stdlib                                                             | 被 patch 成让出 greenlet 的本地 timer，不占 Executor                                                       |

函数不能做的事：

* 直接写 BlockDB。所有写入走 task 声明的 Writer Plugin。
* 绕过 SDK 访问外部服务。审计开启时 `import` 只能拿到白名单模块。
* 跨 call 共享状态。模块命名空间按 task 隔离，call 之间没有顺序。

## 执行预算

* 单个 call 的执行预算是 `CallDeadlineMs = 5000`，只计 CPU 与 IO 后端时间，不含排队；超出即该 call 失败。
* 纯 CPU 循环由 Executor 的 `SIGALRM` 定时器抢占，IO 等待期间 greenlet 让出。
* call 预算与 task 超时相互独立；task 超时以 `TIMED_OUT` 收敛整个 task。

## 静态审计

Worker 的 `FUNCTION_CODE_AUDIT_MODE` 决定源码在派发前是否经过静态审计：

| 模式        | 行为                                                                               |
| --------- | -------------------------------------------------------------------------------- |
| `off`（默认） | 不审计，函数在普通命名空间执行                                                                  |
| `enforce` | 审计不通过或审计器不可用都拒绝派发，该 call 以 `function_code_audit_rejected` 失败且不可重试；通过的代码在受限命名空间执行 |
| `dark`    | 审计并记录日志，但不拦截                                                                     |

审计只做 `ast.parse`，不执行代码。它检查语法节点、`import`、属性访问是否在白名单内，并对返回值和能力边界做类型追踪；受限命名空间只提供 `SAFE_BUILTINS` 和白名单模块的 facade。Sync Invoker 的 `Precheck` 用同一个审计器，只审计不执行，适合在提交前给作者看违规项。完整白名单见 blockx 仓库 `docs/specs/function-code-python-whitelist.md`。

## 小结

* 入口优先按 `functionId` 最后一段找，其次是 `_`，最后是第一个公开 `def`。
* 参数按位置传入，扫描类 Builder 用 `"${表名}"`（`SOURCE_ROW`）占位放入行 dict；返回 `dict` / `list[dict]` / `None`。
* 函数只读；IO 全部经 SDK 拦截回 Worker；写入由 Writer Plugin 完成。
* 每个 task 固定一份代码快照；单 call 预算 5 秒；审计 `enforce` 模式 fail-closed。

继续阅读：

* [Function Code View](/components/function-code)：快照、epoch、syncer 与审计链路的实现。
* [Python Executor](/components/python-executor)：用户函数编程模型、greenlet 挂起与 SDK 拦截点。
* [Call Builder 与 Writer Plugin](/concepts/builders-and-handlers)：`params` 模板与返回值如何落表。
* [提交 task 示例](/development/submit-task-example)：完整可运行的函数与 task。
