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

入口约定

Executor 按下面的优先级在源码的顶层 def 里选入口(python/blockx_audit/tables.pyresolve_entry_name):
  1. functionId 的最后一段。例如注册函数 token.trace_to_token_transfer 的入口是 def trace_to_token_transfer
  2. 名为 _ 的顶层 def,或者一行 _ = <函数名> 别名。blockx-py 把 Python callable 打包成源码时会自动补这行别名。
  3. 源码顺序里第一个不以 _ 开头的顶层 def
inline 源码最简单的写法是直接定义 _
函数体要自包含:模块级常量和辅助函数要写进函数体,用到的模块在函数体内 import

参数与返回值

参数按位置传入,来自 call 的 args 行 dict 的键是源表的列名,值是 JSON 解码后的值。blockx-py 里用 SOURCE_ROW 常量代替手写 "${表名}" 返回值必须能被 JSON 序列化:

函数从哪里来

注册函数的源码由 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。所有写入走 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 决定源码在派发前是否经过静态审计: 审计只做 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。
继续阅读: