Skip to main content

互联网计算机接口规范

简介#

欢迎来到互联网计算机! 我们说 "互联网计算机",是因为尽管在引擎盖下有大量的物理计算机以非微不足道的方式一起工作,但最终我们有一个单一的、共享的、安全的和世界范围内可访问的计算机的外观。许多(如果不是全部)先进和复杂的机器都被隐藏起来,不被那些使用互联网计算机运行他们的应用程序的人和使用这些应用程序的人发现。

目标受众#

本文件描述了互联网计算机的这种外部视图,即它提供给应用程序开发人员和用户的低级接口,以及当他们使用这些接口时会发生什么。

虽然本文档描述了互联网计算机的外部界面和行为,但它并不是最终用户或终端开发者的文档。大多数开发者将通过额外的工具如SDK、Canister开发工具包和Motoko与互联网计算机进行交互。请参阅https://sdk.dfinity.org/,了解合适的内容。

  • 本文档的目标读者是:
    • 使用这些低级接口的人(例如,实现代理、程序罐开发工具包、仿真器、其他工具)。
    • 实现这些低级接口的人(例如,互联网计算机实现的开发者)。
    • 那些想详细了解互联网计算机行为的复杂性的人(如做安全分析)。

提示:本文件是一个严格的、技术上密集的参考资料。它不是互联网计算机的介绍,因此对那些了解高层次概念的人最有用。请先看更高层次的解读。

本文件的范围#

如果你认为互联网计算机是一个分布式执行引擎,提供基于WebAssembly的服务托管服务,那么本文档只描述它的服务托管方面。在可能的范围内,本文档不会谈论区块链、共识协议、节点、子网、正交持久性或治理。

本文试图做到不受实施影响:它同样适用于互联网计算机的(假设的)兼容重新实施。这意味着本文件不包括那些运行互联网计算机的接口(如数据中心运营商、协议开发商、治理用户),因为像节点更新、监控、日志这样的主题本质上是与实际实现及其架构相关的。

互联网计算机的概述#

如果你想作为一个应用开发者使用互联网计算机,你首先要创建一个程序罐模块,其中包含WebAssembly代码和应用的配置,并使用HTTP接口进行部署。你可以使用Motoko语言和SDK来创建程序罐,这更方便。然而,如果你想使用自己的工具,那么本文将描述一个程序罐模块的样子,以及WebAssembly代码如何与系统互动。

一旦你的应用程序在互联网计算机上运行,它就是一个程序罐,用户可以与它互动。他们可以根据系统API使用HTTP接口与程序罐互动。

用户还可以使用HTTP接口发出只读查询,这样做速度更快,但不能改变程序罐的状态。

图1. 互联网计算机的一个典型使用。(这是一个简化的视图;一些箭头代表多个交互步骤或轮询)

"HTTPS接口 "和 "Canister接口(系统API)"两节描述了这些接口,以及对它们的作用的简要描述。之后,你会发现对互联网计算机的更正式的描述,以更严格的方式描述其抽象行为。

命名法#

为了在本文件中获得一些一致性,我们试图精确地使用以下术语。

我们避免使用 "客户端 "这个术语,因为它可能是互联网计算机的客户端,也可能是构成互联网计算机的分布式网络内的客户端。相反,我们使用术语 "用户 "来表示与互联网计算机互动的外部实体,即使在大多数情况下,它将是一些代表(人类)用户的代码(有时称为 "代理")。

程序罐的公共入口点被称为方法。方法可以被声明为更新方法(状态突变被保留)或查询方法(状态突变被丢弃,不能再进行调用)。

方法可以被调用,从调用者到被调用者,最终会产生一个响应,要么是返回,要么是拒绝。一个方法可以有参数,这些参数在方法调用中以具体的参数提供。

罐之间的调用不区分更新方法和查询方法;罐之间的调用可以保留状态突变,因此类似于更新方法调用。注意,从一个罐到它自己的调用也算作 "罐"。外部调用可以是更新调用,它可以调用两种方法,也可以是查询调用,它只能调用查询方法。

在内部,调用或响应是以消息的形式从发送方传送到接收方的。消息没有响应。

WebAssembly函数是由WebAssembly模块导出的,或者由系统API提供。这些函数被调用,可以捕获或返回,可能还有一个返回值。函数也有参数并接受参数。

外部用户通过在HTTPS接口上发出请求与系统互动。请求有响应,可以是回复,也可以是拒绝。有些请求会导致内部信息的产生。

程序罐和用户由一个委托人识别,有时也称为ID。

通俗的概念#

在进入本文描述的四个公共接口(即面向代理的HTTPS接口、面向程序罐的系统API、虚拟管理程序罐和系统状态树)的细节之前,本节将介绍一些超越多个接口的概念。

未指定的常量和限制

本规范可能会提到某些常量和限制,但没有指定它们的具体数值(还没有),也就是说,它们是由实现定义的。许多是资源限制,它们只与指定系统的错误处理行为有关(如上所述,本文档中也尚未精确描述)。这个列表并不完整。

max_cycles_per_message

在试图执行一条消息之前,一个程序罐必须有的循环次数,在执行消息之前从程序罐的余额中扣除。参见消息执行。

max_cycles_per_response(最大Cycle)。

当一个程序罐执行调用时,系统预留的Cycle数量。这是用来支付处理响应消息的费用,执行响应后未使用的Cycle会被退还。参见消息执行。

max_canister_balance

程序罐Cycle的最大余额。任何多余的都被丢弃。小于264。

主体(Principals ID)#

主体是程序罐、用户以及未来可能的其他概念的通用标识。就系统的大多数用途而言,它们是不透明的二进制字节,长度在0到29个字节之间,而且故意没有机制来区分罐之间ID和用户ID。

然而,它们有一些结构来编码特定的认证和授权行为。

原则的特殊形式

在本节中,H表示SHA-224,-表示blob连接,|p|表示p的长度(字节),编码为一个字节。

有几类id:

1.不透明的ID

这些ID总是由系统生成的,在系统外没有任何相关的结构。

通常情况下,这些ID以字节0x01结尾,但是IC的用户不需要关心这个。

2.自我认证的ID,这些ID的形式是H(public_key) - 0x02 (29字节)。如果外部用户拥有相应的私钥,他们可以使用这些ID作为请求的发送者。公钥使用签名中描述的编码之一。

3.派生的ID,这些ID的形式为H(|registering_principal| - registering_principal - derivation_nonce) - 0x03 (29 bytes)。当需要注册一个 id 时,这些 id 会被特殊处理。在这样的请求中,任何请求 id 的人都可以提供derivation_nonce. 通过将其与调用者的主体一起散列,每个主体都有一个 id 空间,只有他们可以从中注册 id。(提示:本文档中当前未明确使用派生 ID,但它们可能会在内部或将来使用。)

4.匿名 ID,它的形式为0x04,用于匿名调用者。它可以在没有签名的情况下用于调用和查询请求。

当系统创建一个新的id 时,它永远不会创建一个自我认证的 id、一个匿名 id 或一个从可能是罐或用户的东西派生的 id。

委托人的文本表示法#

这种文本表示法实际上并没有出现在界面中(界面总是处理Blobs),所以它只是一个推荐的惯例。

我们指定了一种典型的文本格式,每当需要以文本格式打印或读取原则时,我们都会推荐这种格式,例如,在日志信息、事务浏览器、命令行工具、源代码中。

  • 一个blob的文本表示是Grouped(Base32(CRC32(b)-b)),其中:
    • CRC32是一个四字节的校验序列,按照ISO 3309、ITU-T V.42和其他地方的定义计算。
    • Base32是RFC 4648中定义的Base32编码,没有添加填充字符。
    • 中间的点表示连接。
    • Grouped采用ASCII字符串,每5个字符插入分隔符-(破折号)。最后一组可能包含少于5个字符。分隔符从不出现在开头或结尾。

文本表示法通常以小写字母打印,但在解析时不考虑大小写。

因为一个委托人的最大尺寸是29字节,所以文本表述将不超过63个字符(10乘以5加上3个字符,中间有10个分隔符)。

ID为0xABCD01的程序罐的校验序列为0x233FF206(在线计算器);因此最终的ID为em77e-bvlzu-aq。

在bash中从十六进制编码和解码到十六进制的例子(以下内容可以原样粘贴到终端)。

function textual_encode() {
( echo "$1" | xxd -r -p | /usr/bin/crc32 /dev/stdin; echo -n "$1" ) |
xxd -r -p | base32 | tr A-Z a-z |
tr -d = | fold -w5 | paste -sd'-' -
}
function textual_decode() {
echo -n "$1" | tr -d - | tr a-z A-Z |
fold -w 8 | xargs -n1 printf '%-8s' | tr ' ' = |
base32 -d | xxd -p | tr -d '\n' | cut -b9- | tr a-z A-Z
}

程序罐的生命Cycle#

  • 互联网计算机上的服务被称为程序罐。从概念上讲,它们由以下几块状态组成:

    • 一个程序罐的ID(一个委托人)
    • 它们的所有者(一个可能是空的委托人列表)
    • 一个Cycle余额
    • 程序罐的状态,它是运行、停止或停止中的一种。
    • 资源保留
  • 一个程序罐可以是空的(例如,直接在创建后)或非空的。一个非空的罐也有:

    • 代码,以程序罐模块的形式存在
    • 状态(内存、全局等)。
    • 可能还有系统内部的数据(例如队列)

程序罐在创建和卸载后是空的,通过代码安装成为非空的。

如果一个空的程序罐收到了一个响应,该响应就会被丢弃,就像程序罐在处理该响应时被困住一样。为其处理预留的Cycle和响应中携带的Cycle被添加到程序罐的Cycle平衡中。

程序罐的Cycle#

IC依靠Cycle,一种实用的代币,来管理其资源。一个程序罐用它的Cycle余额来支付它所使用的资源。

  • 当一个程序罐的Cycle余额下降到零时,该程序罐就被取消分配。这与以下情况有相同的效果:
    • 卸载程序罐(如IC方法uninstall_code中所述)
    • 将所有的资源保留设置为零

此后,贮藏室是空的。在补足其余额后可以重新安装。

提示:一旦IC释放了一个程序罐的资源,它的ID、Cycle余额和控制器将在IC上至少保存10年。在这段时间之后,滤毒罐会发生什么,目前还没有明确的规定。

罐的状态#

  • 程序罐的状态可以用来控制程序罐是否在处理调用:
    • 在运行状态下,对罐的请求被正常处理。
    • 在状态停止时,系统会拒绝对程序罐的调用,但对程序罐的响应会正常处理。
    • 在停止状态下,对程序罐的调用被系统拒绝,并且没有未完成的响应。

在所有情况下,不管被管理的程序罐处于什么状态,对管理程序罐的调用都被处理。

程序罐的控制器可以使用stop_canister和start_canister来启动这些状态之间的转换,并使用canister_status查询状态。程序罐本身也可以使用ic0.canister_status查询其状态。

注意:这个状态与程序罐是否为空的问题是正交的:一个空的程序罐可以处于运行状态。对这样一个程序罐的调用仍然会被拒绝,但是因为该程序罐是空的。

签名#

数字签名方案用于验证IC基础设施各部分的信息。签名是域分离的,这意味着每条消息都有一个字节串作为前缀,该字节串对于签名的目的是独一无二的。

IC支持多种签名方案,细节在下面的小节中给出。对于每个方案,我们指定公钥编码的数据(总是DER编码的,并指出要使用的方案)以及签名的形式(在本规范的其余部分,它是不透明的blobs)。

在所有情况下,签名的有效载荷都是域分离器和消息的连接。本规范中对签名的所有使用都表示一个域分离器,以唯一地识别签名的目的。域分离器在结构上是无前缀的,因为它们的第一个字节表示其长度。

Ed25519 和 ECDSA 签名

  • 普通签名支持以下方案:
    • Ed25519或
    • ECDSA在曲线P-256(也称为secp256r1)上,使用SHA-256作为散列函数,以及在Koblitz曲线secp256k1上。
    • 公钥必须对签名方案Ed25519或ECDSA有效,并被编码为DER。
      • 关于Ed25519公钥的DER编码见RFC 8410。
      • ECDSA公钥的DER编码见RFC 5480;DER编码不能指定哈希函数。对于曲线secp256k1,使用OID 1.3.132.0.10。点必须以未压缩的形式指定(即0x04后面是x和y的32字节的大安培编码)。
    • 签名被编码为两个值r和s的32字节大恩典编码的连接。

网络认证#

唯一允许用于网络认证的签名方案是:P-256曲线上的ECDSA(也被称为secp256r1),使用SHA-256作为哈希函数。

签名是通过使用有效载荷作为网络认证断言中的挑战来计算的。

签名是通过验证挑战字段包含有效载荷的base64url编码来检查的,并且签名验证于认证者数据 - SHA-256(utf8(clientDataJSON)),如WebAuthn w3c建议中规定的。

公钥被编码为DER包裹的COSE密钥。

它使用用于其他类型的公钥的SubjectPublicKeyInfo类型(例如,见RFC 8410,第4节),OID为1.3.6.1.4.1.56387.1.1(iso.org.dod.internet.private.enterprise.dfinity. mechanisms.der-wrapped-cose)。BIT STRING字段subjectPublicKey包含COSE编码。有关 COSE 编码的详细信息,请参见 WebAuthn w3c 建议或 RFC 8152。

COSE密钥的DER包装如下所示。它可以通过命令 sed "s/#.*//" 进行解析。| xxd -r -p | openssl asn1parse -inform der.

30 5E # SEQUENCE of length 94 bytes
30 0C # SEQUENCE of length 12 bytes
06 0A 2B 06 01 04 01 83 B8 43 01 01 # OID 1.3.6.1.4.1.56387.1.1
03 4E 00 # BIT STRING encoding of length 78,
A501 0203 2620 0121 5820 7FFD 8363 2072 # length is at byte boundary
FD1B FEAF 3FBA A431 46E0 EF95 C3F5 5E39 # contents is a valid COSE key
94A4 1BBF 2B51 74D7 71DA 2258 2032 497E # with ECDSA on curve P-256
ED0A 7F6F 0009 2876 5B83 1816 2CFD 80A9
4E52 5A6A 368C 2363 063D 04E6 ED

您还可以在一个在线的ASN.1 JavaScript解码器中查看包装。

  • 签名是一个CBOR值,由一个主要类型为6("语义标签")、标签值为55799的数据项组成(见自我描述CBOR),后面是一个包含三个强制性字段的映射。
    • authenticator_data(blob)。WebAuthn认证器数据。
    • client_data_json(文本)。WebAuthn客户端数据的JSON表示。
    • signature (blob)。WebAuthn w3c建议中规定的签名,在ECDSA签名的情况下,这意味着DER编码。

程序罐的签名#

IC还支持一种方案,即程序罐可以通过声明一个特殊的 "认证变量 "来签署一个有效载荷。

本节向前引用了本文件中的其他概念,特别是认证部分。

公钥是一个DER包裹的结构,表示签名的程序罐,并包括一个可自由选择的种子。每一个种子的选择都会产生一个不同的公钥,而且签名者可以选择在种子中编码信息,如用户ID。

更具体地说,它使用用于其他类型的公钥的SubjectPublicKeyInfo类型(例如,见RFC 8410,第4节),OID为1.3.6.1.4.1.56387.1.2(iso.org.dod.internet.private.enterprise.dfinity. mechanisms.canister-signature)。

BIT STRING字段subjectPublicKey是blob|signing_canister_id| - signing_canister_id - seed,其中|signing_canister_id|是signing_canister_id长度的单字节编码,-表示blob连接。

  • 签名是一个CBOR值,由一个主要类型为6("语义标签")、标签值为55799的数据项组成(见自我描述CBOR),后面是一个有两个必选字段的映射。
    • 证书(blob)。按照证书的编码,一个CBOR编码的证书。
    • 树(hash-tree)。根据证书编码,是一棵哈希树。

给定一个有效载荷以及上述格式的公钥和签名,可以通过检查以下两个条件来验证签名。

该证书必须是认证中所述的有效证书,具有:lookup(/canister/<signing_canister_id>/certified_data, certificate.tree) = Found (reconstruct(tree))

其中signing_canister_id是签名罐的 id,reconstruct是一个计算树的根哈希的函数。

本tree必须是一个well_formed与树:lookup(/sig/<s>/<m>, tree) = Found ""

其中s是seed公钥m中使用的 SHA-256 哈希值, 是有效负载的 SHA-256 哈希值。

系统状态树#

系统状态的部分内容以验证的方式公开暴露(例如通过Request: Read state或Certified data)(见认证机制的认证)。本节将抽象地描述系统状态的内容。

从概念上讲,系统状态是一棵树,树上有标记的子,树叶上有值。等价地,系统状态是一个从路径(标签序列)到值的映射,其中域是无前缀的。

标签始终是Blobs(但通常有一个人类可读的表述)。在本文中,路径是用斜线作为分隔符来写的;实际的编码并不使用斜线作为分隔符,而且标签可以包含0x2F字节(ASCII /)。值是自然数、文本值或blob值。

本节规定了树中公开的相关路径。

  • 时间

    • /时间(自然数)。
      • 所有的部分状态树都包括一个时间戳,表示该状态是当前的时间。
  • 子网信息

    • 状态树包含互联网计算机拓扑结构的信息。
      • /subnet/<subnet_id>/public_key (blob)
      • 子网的公钥(DER编码的BLS密钥,见认证)。
  • 请求状态

  • 对于系统已知的每个异步请求,其状态都在/request_status/<request_id>的子树中。关于异步请求如何工作的更多细节,请参见 "罐头调用概述"。

    • /request_status/<request_id>/status (text),接收、处理、回复、拒绝或完成中的一种,关于每种状态的详细含义,请参见罐式呼叫概述。
    • /request_status/<request_id>/reply (blob),如果状态是回复,那么这个路径包含回复blob,否则就不存在。
    • /request_status/<request_id>/reject_code (自然),如果状态是拒绝,那么这个路径包含拒绝代码(见拒绝代码),否则就不存在了。
    • /request_status/<request_id>/reject_message (text),如果状态是拒绝,那么这个路径包含一个文本诊断信息,否则不存在。

在提交一个请求后,请求可能还没有显示出来,因为系统还在努力接受请求为待定。

请求状态实际上不会被无限期地保留,最终系统会忘记这个请求。这种情况不会早于请求的过期时间,这样可以防止重放攻击,但可能会更久,这样用户就有机会获取它。精确的政策还没有定义。

认证数据 /canister/<canister_id>/certified_data(blob),具有给定id的程序罐的认证数据,见认证数据。

程序罐信息

用户可以通过认证的方式了解程序罐模块的哈希值和它当前的控制器。/canister/<canister_id>/module_hash(blob)。

如果程序罐是空的,这个路径就不存在。如果程序罐不是空的,它就存在,并且包含当前安装的程序罐模块的SHA256哈希值。参照IC方法canister_status。/canister/<canister_id>/controllers(blob)。

罐的当前控制器。该值由一个CBOR数据项组成,主要类型为6("语义标签"),标签值为55799(见自我描述CBOR),后面是二进制形式的原则数组(CDDL #6.55799([* bytes .size (0..29)]))

HTTPS接口#

用户用来向互联网计算机发送请求的具体机制是通过HTTPS API,它暴露了三个端点来处理与互联网计算机的互动,外加一个用于诊断。

/api/v2/canister/<effective_canister_id>/call,用户可以提交(异步的、状态变化的)调用。

/api/v2/canister/<effective_canister_id>/read_state,用户可以读取各种系统信息。特别是,他们可以在这里轮询一个调用的状态。

/api/v2/canister/<effective_canister_id>/query,用户可以执行(同步的,非状态变化的)查询调用。

/api/v2/status,用户可以获得关于网络的额外信息。

在这些路径中,<effective_canister_id>是有效程序罐ID的文本表示。

/api/v2/canister/<effective_canister_id>/call/api/v2/canister/<effective_canister_id>/read_state和/api/v2/canister/<effective_canister_id>/query的请求是带有CBOR编码的请求体的POST请求,它包括一个认证封套(按照认证)和下面描述的请求特定内容。

本文件还没有解释如何找到互联网计算机的位置和端口。

序罐呼叫的概述

用户通过调用程序罐与互联网计算机交互。根据分布式实现的本质,它们不能被立即执行,而只能被延迟执行。此外,用户与之交谈的实际节点可能并不诚实,或者由于其他原因,可能无法在路上得到请求。这意味着以下的高级工作流程。

一个用户通过HTTPS接口提交一个调用。节点没有返回任何有用的信息(因为这种信息无论如何都是不可信的)。

在一定时间内,系统表现得好像不知道这个呼叫一样。

系统询问目标程序罐是否愿意接受这个消息,并被收取处理该消息的费用。这使用了正常呼叫的Ingress消息检查API。对于对管理罐的调用,适用IC管理罐中的规则。

在某些时候,系统可能会接受该调用进行处理,并将其状态设置为接收。这表明系统作为一个整体已经收到了该呼叫,并计划对其进行处理(尽管如果系统处于高负载状态,它仍然可能无法得到处理)。此外,用户也应该能够向任何一个端点询问待处理呼叫的状态。

一旦明确该呼叫将被采取行动(有足够的资源,呼叫尚未过期),状态就会变为处理。现在,用户可以保证该请求将产生效果,例如,它将到达目标程序罐。

现在,系统正在处理该调用。对于一些调用来说,这可能是原子性的,对于其他调用来说,这涉及到多个内部步骤。

最终,将产生一个响应,并可以在一定时间内被检索到。响应要么是回复,表示成功,要么是拒绝,表示某种形式的错误。

在调用被保留了足够长的时间,但请求还没有过期的情况下,系统可以忘记响应数据,只记住调用完成的情况,以防止重放攻击。

一旦过了有效期,系统就可以修剪该呼叫和它的响应,并完全忘记它。

这就产生了下面的交互图:

TODOTODO