# 输入、证明者提示与公开输出

> 客户程序没有 I/O 系统调用。它的公开输入、证明者提示和公开输出是三个内存区域。各自存放什么、证明绑定什么，以及每个接收数据的客户程序都遵循的模式。

远地虚拟机的客户程序（guest）没有文件描述符、没有流，也没有 I/O 系统调用。它的输入和输出是三个内存区域，用普通的加载和存储指令读写，证明绑定其中的两个。

## 三个内存区域

| 区域 | SDK | 内容 | 大小 | 是否受证明绑定 |
| --- | --- | --- | --- | --- |
| 公开输入 | `public_input()`、`read_input(buf)` | 陈述中的字节，由请求证明的一方选择 | 最多 16,380 字节 | 是，绑定其初始内容 |
| 证明者提示（advice） | `advice()` | 证明者选择的字节 | 最多 2 GiB | **否** |
| 公开输出（journal） | `commit(bytes)`、`journal()` | 客户程序追加写入的内容 | 最多 16,380 字节 | 是，绑定其最终内容 |

```rust
let input: &[u8] = guest_sdk::public_input(); // a slice over the input window, no copy
let data: &[u8] = guest_sdk::advice();        // a slice over the advice region
guest_sdk::commit(b"result");                  // appends to the journal
```

- `public_input()` 和 `advice()` 返回的是内存上的切片，不发生任何复制。`read_input(buf)` 复制 `min(buf.len(), input.len())` 个字节并返回实际数量，所以返回值可能小于请求的长度。
- `commit` 追加数据并维护一个长度字，正是这个长度字让证明绑定一个确切的字节串，而不是一个用零填充的窗口。它宁可以状态 **70** 退出，也不会让窗口溢出。
- 在没有提供证明者提示的运行中调用 `advice()`，会导致致命的 `OutOfBounds`，而不是返回空切片：没有证明者提示的运行根本没有证明者提示区域，也不必为它付出任何代价。

## “绑定”是什么意思

证明所确立的陈述，带有公开输入的字节、公开输出的字节和退出状态。证明表明：在客户程序第一次访问之前，输入窗口存放的恰好是陈述中的输入；客户程序退出时，公开输出窗口存放的恰好是陈述中的输出。这一点依靠的是内存论证，而不是客户程序做的任何事：客户程序不必计算任何哈希，也不必遵循任何约定。

证明者提示则不同。证明者提示区域的初始内容是证明者写进去的任何东西，没有任何东西把它与程序身份、陈述或任何门联系起来。证明只说明：存在*某份*证明者提示，使程序在这个输入下发布了这份公开输出。这一保证的强度，恰好等于客户程序自己对证明者提示所做检查的强度。

## 模式：承诺、提供、检查

输入较大的客户程序，把大部分数据作为证明者提示接收，由公开输入对它做出承诺；在任何由证明者提示推导出的内容进入公开输出之前，先把两者相互核对：

```rust title="先校验证明者提示，再信任它"
fn main() {
    let want = guest_sdk::public_input(); // 32 bytes: keccak256 of the advice
    let data = guest_sdk::advice();       // the prover's bytes, bound by nothing
    if guest_sdk::keccak256(data).as_slice() != want {
        guest_sdk::exit(1); // refused before anything derived from it is committed
    }
    let sum = data.iter().fold(0u32, |s, b| s.wrapping_add(u32::from(*b)));
    guest_sdk::commit(&sum.to_le_bytes()); // the journal: what the proof publishes
}
```

这项检查不一定是对整个证明者提示求哈希。它可以是对照输入所携带的根来检查的 Merkle 路径，就像[账本示例](https://apogee.gweb3networks.com/docs/blockchain-native#ledger-native)那样；也可以是对数据的签名；或者是结果本身满足的约束，例如证明者声称的排序顺序，由客户程序用一遍扫描来验证，而不必自己排序。关键在于，用来核对的那样东西必须是受绑定的。

> [!CAUTION]
> 提交未经检查的证明者提示的任何函数值，就等于发布一个由证明者选定的值。这是写出一个证明毫无意义的客户程序最常见的方式。

## 结构化数据

用 `no_std` 序列化器对结构化输入编码，例如基于 `serde`、启用 `alloc` 特性的 `postcard`；仓库自己的以太坊客户程序就用它来编码区块见证。两个习惯能让格式保持严谨：

- **使用定宽整数。** 用 `u32` 和 `u64`，绝不用 `usize`，它的宽度在你的宿主机（host）和客户程序之间不同。
- 在要紧的地方，**坚持每个值只有一种编码**。接受尾随字节或非最短变长整数（varint）的反序列化器，会让同一个值对应两个字节串。在唯一性要紧的地方，先解码、再重新编码并比较，以太坊客户程序的 `BlockWitness::decode` 就是这样做的。

## 会增长的输出

公开输出最多容纳 16,380 字节。随工作量增长的输出，例如每笔交易一条记录，没有固定的上界，迟早会以 70 退出。改为发布摘要：边生成记录边对其求哈希，提交 32 字节的结果，再让需要这些记录的一方在原生环境中重新计算并比对。仓库中的无状态以太坊校验器就是这样为整个区块发布一份 43 字节的公开输出。

对于要在以太坊上检验的证明，两个公开值都要保持**固定长度**。部署的验证者合约是针对一个输入长度和一个输出长度构建的，任何其他长度都会被拒绝（[链上结算](https://apogee.gweb3networks.com/docs/launch/on-chain#shape)）。

## 退出状态

退出时，除了已经提交的内容之外，不会发布任何东西；而发生 panic 或以非零状态退出的运行，对它所做的事有一份有效的证明。所以验证者先读退出状态，再读公开输出。在链上，验证者合约把预期的状态作为参数，应用传入 `0`。SDK 自身使用的状态：

| 状态 | 含义 |
| --- | --- |
| 0 | `main` 返回，或 `exit(0)` |
| 70 | `commit` 将超过 16,380 字节 |
| 71 | 某次分配将触及栈：参见[堆](https://apogee.gweb3networks.com/docs/launch/write#heap) |
| 72 | 某个委托返回了其 shim 拒绝接受的结果 |
| 101 | panic，不打印任何内容 |

自定义的失败代码要避开这些值，就像账本示例使用 1、2 和 3 那样。

## 证明没有说明什么

- **没有任何东西规定公开输出的写入顺序**，也没有任何东西强制客户程序读取它的输入。证明绑定的是窗口的内容，而不是产生这些内容的访问。
- **证明者提示是可写的。** 向证明者提示区域写入就是一次普通的存储。无论写与不写，它都不受绑定。
- **公开输出就是窗口的全部最终内容。** `commit` 会维持这种格式。直接写这个窗口的客户程序必须自己保持它：一个不超过 16,380 的长度，随后是这么多字节，再之后全是零。

规范对这一切有精确的表述：[公开值与证明者提示](https://apogee.gweb3networks.com/docs/auditors/spec/public-values)。
