在多方计算对交易进行签名之前,它会根据实例中的交易政策对交易进行评估。本文档介绍了要评估哪些政策、政策如何在其生命周期中移动、政策可以检查什么以及交易被拒绝的原因。
如何评估交易
事务政策是单个 Cedar
permit 或 forbid 语句。当签名者调用 CreateTransaction 时,实例会使用 Transaction.schema 的解析器解码 Transaction.payload 中的未签名交易,并在使用任何密钥共享之前根据交易评估每项强制执行的政策。当政策的状态为 ACTIVE 或 PENDING_DISABLED 时,系统会强制执行该政策。如需了解详情,请参阅政策生命周期。
该决策遵循以下三条规则:
- 默认拒绝。只有在至少有一项
permit政策与事务匹配时,才允许该事务。如果没有强制执行的政策,每笔交易都会被拒绝。 - 禁止获胜。如果有任何
forbid政策匹配,则事务会被拒绝,即使permit政策也匹配。 - 无法评估的政策不匹配。例如,如果某项政策读取事务不具有的可选上下文属性,则该政策无法求值。实例会记录错误,跳过该政策,并根据其余强制执行的政策做出决定。这也适用于
forbid政策,因此请编写不会因缺少属性而失败的forbid政策。如需查看示例,请参阅场景 8 和场景 9。
对于包含多笔转账的交易(例如包含多条转账指令的 Solana 交易),系统会针对每笔转账分别进行评估。只有在允许每项转移的情况下,才允许执行事务。如果任何转账被拒绝或无法解码,则整个交易都会被拒绝。
每次评估(允许或拒绝)都会生成一个评估记录,该记录会在 Transaction.policyEvaluationRecord 中的事务上返回。
该决策控制实例是否可以在不等待更多签名者的情况下对事务进行签名:
PERMIT:如果CreateTransaction请求已包含足够的签名者授权,可以满足 KeyRing 的密钥政策,则交易会立即进入SIGNING状态。否则,它会在PENDING_AUTHORIZATION中创建,并等待更多签名者。DENY:实例不会自动对事务进行签名。该事务在PENDING_AUTHORIZATION中创建,policyEvaluationRecord.decision设置为DENY。签名者可以查看评估记录并拒绝交易(RecordSignerDecision带有DENY)或无论如何都授权交易 (AUTHORIZE)。只有在有足够多的签名者授权交易以满足密钥政策时,交易才会被签名。
交易政策是自动签名的护栏,而不是对签名者的限制:签名者可以授权政策拒绝的交易。原始 PolicyEvaluationRecord 仍会附加到交易中以供审核。
政策生命周期
交易政策无法更改。TransactionPolicy 没有更新方法,其 rule 不可变。如需更改规则,请使用 DisableTransactionPolicy 停用政策,然后使用 CreateTransactionPolicy 创建新政策。
启用或停用政策需要获得法定人数的政策作者(在 User.roleStatuses 中具有有效 POLICY_AUTHOR 角色的用户)的批准。每位政策作者都会签署一份 TransactionPolicySigningPayload,其中指明政策(或 CreateTransactionPolicy 的实例)、操作(ACTIVATE 或 DISABLE)和 rule.cedarDocument。提出更改的请求算作第一个批准。其他政策作者使用 ApproveTransactionPolicy 进行审批。所需的审批次数介于 activationInfo.requiredApprovals 到 deactivationInfo.requiredApprovals 之间。如果实例只需要一个批准,则更改会立即生效。
下表介绍了每种政策状态以及是否强制执行处于该状态的政策:
| 州 | 含义 | 是否强制执行? |
|---|---|---|
PENDING_ACTIVE |
该政策已提出,正在等待政策作者批准。 | 否 |
ACTIVE |
该政策有 activationInfo.requiredApprovals 项审批。 |
是 |
PENDING_DISABLED |
政策作者请求停用该政策,并且该请求正在等待审批。 | 是,直到请求获得 deactivationInfo.requiredApprovals 个批准 |
DISABLED |
该政策已被停用,或在生效前被撤回。已停用的政策会保留以供审核。 | 否 |
政策会按一个方向经历以下状态:
PENDING_ACTIVE到ACTIVE:激活已获得足够的批准。PENDING_ACTIVE到DISABLED:政策作者在政策生效之前使用DisableTransactionPolicy撤消政策。ACTIVE到PENDING_DISABLED:政策作者调用DisableTransactionPolicy。PENDING_DISABLED到DISABLED:停用请求已获得足够的批准。
一项政策最多只能进入每个待处理状态一次。已停用的政策无法再次启用;请改为创建新政策。
如需了解哪些人可以调用这些方法,请参阅用户角色。
政策集版本
政策集是指在某个时间点强制执行(ACTIVE 或 PENDING_DISABLED)的一组政策。每次激活或停用完成时,实例都会为该转换分配实例计数器中的下一个数字,并将其记录在 activationInfo.transitionSequence 或 deactivationInfo.transitionSequence 中。该数字是转换生成的政策集版本。
下表显示了政策集版本如何在一系列事件中发生变化:
| 事件 | 过渡序列 | 强制执行的政策 |
|---|---|---|
| 政策 A 已启用。 | A: activationInfo.transitionSequence = 1 |
A |
| 政策 B 已启用。 | B: activationInfo.transitionSequence = 2 |
A、B |
政策 C 被提议 (PENDING_ACTIVE),然后被撤回。 |
无 | A、B |
| 政策 A 的停用请求获得批准。 | A: deactivationInfo.transitionSequence = 3 |
B |
撤消 PENDING_ACTIVE 政策绝不会更改强制执行的政策集,因此不会获得转换序列,也不会更改政策集版本。当转换正在等待审批时,其 transitionSequence 未设置。
每次评估都会读取事务 createTime 时的政策、KeyList 和密钥,并在 policyEvaluationRecord.policySetVersion 中记录当时已完成的最高转换序列。如果两个评估具有相同的政策集版本,则它们使用了相同的政策。
Cedar 请求
Cedar 请求包含主账号、操作、资源和上下文。例如:
permit (
principal == User::"alice@example.com",
action == Action::"buy_transaction",
resource == Resource::"transaction_management"
)
when {
context.principal_keyring == "institutional-treasury" &&
context has fiat_amount && context.fiat_amount <= 10000
};
用户如何映射到 Cedar 主账号
主账号是事务的发起者:其 principal 与 CreateTransaction 调用方的经过身份验证的身份匹配的用户。该用户必须具有有效的 SIGNER 角色。在 Cedar 中,主账号是一个 User 实体,其 ID 是用户的 principal 字段,例如 User::"alice@example.com"。
该实例不会向 Cedar 传递任何实体数据。User 实体没有属性,也没有父实体,因此:
principal == User::"alice@example.com"与 Alice 发起的事务匹配。principal in Team::"treasury"永远不匹配。如需限制谁可以使用密钥进行签名,请使用 KeyRing 的密钥政策。如需将规则范围限定为一组密钥,请勾选context.principal_keyring。- 读取
principal的属性(例如principal.team)的规则无法求值。
用户的 principal 无法更改,因此命名主账号的规则会一直引用同一身份。
操作和资源
action:CreateTransaction请求中Transaction.action的值,格式为 CedarAction实体。CreateTransaction接受以下操作:操作 用于: Action::"buy_transaction"获取资产的交易。 Action::"sell_transaction"处置资产的交易。 Action::"internal_transfer_transaction"资产转移,例如付款或密钥之间的清扫。 发起者选择操作;多方计算不会从载荷中推断操作。规则可以匹配一项操作 (
action == Action::"buy_transaction")、一系列操作 (action in [Action::"buy_transaction", Action::"sell_transaction"]),也可以不限制action以匹配每项操作。resource:对于事务评估,始终为Resource::"transaction_management"。规则可以检查resource == Resource::"transaction_management",也可以让resource不受限制。
上下文属性
上下文包含政策检查的属性。它们来自两个地方:
- 交易载荷和签名密钥。实例会解码
Transaction.payload,并添加签名密钥及其 KeyRing 的 ID。这些属性包括principal_keyring、principal_key、blockchain、schema、secondary_key、amount_token_scaled、token_symbol、contract_address、chain_id、max_fee_atomic、payload和time_stamp。如需查看完整表格和每个区块链的行为,请参阅写入事务政策规则。 - 事务的
metadata字段,由发起方在CreateTransaction中设置。
下表介绍了元数据属性:
Transaction.metadata 字段 |
Cedar 属性 | 雪松类型 | 值 |
|---|---|---|---|
fiatAmount |
context.fiat_amount |
Long |
交易价值,以美元 (google.type.Money) 为单位。currencyCode 必须为 USD,units 必须大于或等于零,nanos 必须为 0。 |
validity |
context.validity |
Long |
交易的有效时长 (google.protobuf.Duration),以整秒为单位。必须大于 0 秒,且不含秒的小数部分。 |
principalValue1 |
context.principal_value1 |
String |
您定义的任何值,最多 256 个字符,例如交易平台或成本中心。 |
principalValue2 |
context.principal_value2 |
String |
您定义的任何值,最多 256 个字符,例如内部工单或交易对手 ID。 |
以下规则适用于元数据:
- 未设置的字段不在上下文中。如果事务没有
fiatAmount,则context.fiat_amount不存在,并且读取该事务的政策无法评估。首先使用has检查属性,如context has fiat_amount && context.fiat_amount <= 10000所示。 - 元数据无效会导致请求失败。如果某个字段违反了上表中的规则,例如
EUR中存在fiatAmount或nanos非零、validity为 0 秒或principalValue1超过 256 个字符,则CreateTransaction会失败并显示INVALID_ARGUMENT,并且不会评估任何政策。 - 实例不会根据事务检查元数据。
fiatAmount是发起方认为的交易价值。其他签名者在授权之前,会在Transaction.metadata和policyEvaluationRecord.evaluationContext中看到元数据。如需独立于元数据限制链上金额,请检查context.amount_token_scaled和context.contract_address。 - 创建交易后,元数据无法更改。
密钥环
KeyRing 用于对密钥进行分组。每个事务的上下文都有 principal_keyring 和 principal_key,因此规则可以应用于 KeyRing 中的每个密钥,包括稍后创建的密钥,并且 KeyRing 可以是 KeyList 中的一个条目。如需了解详情和示例规则,请参阅 KeyRings。
KeyLists
KeyList 是一个命名的区块链地址列表,政策可以引用该列表,例如作为目标许可清单。每个条目都是以下项之一:
- 手动条目 (
manualEntry):实例不管理的地址,具有可选的source和reason字段。 - 实例中的密钥 (
key):与密钥的地址匹配。 - 实例中的 KeyRing (
keyRing):与 KeyRing 中每个有效密钥的地址匹配,包括在添加条目后创建的密钥。
KeyList 可能会在政策获批后发生变化。当您使用 AddKeyListEntries 或 RemoveKeyListEntries 添加或移除条目时,引用 KeyList 的每个政策都会在下次评估时使用新条目,而无需新的政策作者批准。当 PENDING_ACTIVE、ACTIVE 或 PENDING_DISABLED 政策引用 KeyList 时,无法使用 DeleteKeyList 将其删除。
Cedar 环境中的 KeyList
在评估时,实例会将每个 KeyList 展开为一个包含 String 个区块链地址的扁平 Set:手动条目,以及从每个密钥和 KeyRing 条目派生的地址。每个集合都会作为 context.key_lists["<var>KEY_LIST_ID</var>"] 传递给 Cedar,其中 KEY_LIST_ID 是 KeyList 资源名称中的 {keyList} ID,例如 projects/my-project/locations/us-central1/instances/my-instance/keyLists/trusted-destinations 的 trusted-destinations。
规则会使用 Cedar contains 运算符针对解码后的目标地址 context.secondary_key 测试成员资格。例如,如需仅允许向 trusted-destinations KeyList 中的地址进行转移,请运行以下命令:
// Assuming you created a KeyList with ID "trusted-destinations":
permit (principal, action, resource)
when { context.key_lists["trusted-destinations"].contains(context.secondary_key) };
如需拒绝向 blocked-addresses KeyList 中的任何地址进行转移,请执行以下操作:
// Assuming you created a KeyList with ID "blocked-addresses":
forbid (principal, action, resource)
when { context.key_lists["blocked-addresses"].contains(context.secondary_key) };
请注意以下几点:
- 对于 KeyList ID,请使用方括号语法,而不是点语法。当 ID 包含连字符(Cedar 会将其解读为减法)或以数字开头(如服务器生成的 UUID)时,Cedar 的点语法 (
context.key_lists.trusted-destinations) 无法解析。方括号语法context.key_lists["<var>KEY_LIST_ID</var>"]适用于每个 ID。 key_lists始终存在,规则可以直接读取其中的 KeyList。CreateTransactionPolicy会验证规则引用的每个 KeyList 是否存在,并且DeleteKeyList会拒绝删除政策引用的 KeyList。仅当规则引用可能尚不存在的 KeyList 时,才使用context.key_lists has "<var>KEY_LIST_ID</var>"。- 地址会作为字符串进行比较。
secondary_key使用写入事务政策规则为每个区块链描述的格式,例如以太坊的大小写混合 EIP-55 校验和格式。以相同格式手动添加条目。 - Solana 代币转账的
secondary_key是目标代币账号,而不是所有者的钱包。如需允许某个钱包,请为您允许的每个铸币添加其代币账号。
每次评估都会在事务的 createTime(与政策相同的时间)解析 KeyList 成员资格,并将结果记录在 policyEvaluationRecord.keyListMemberships 中:KeyList、测试的地址、是否为成员以及匹配的条目。
由于 KeyList 会更改政策允许的内容,因此请限制可以更改它的人员。
KeyList 方法需要 roles/mpc.admin。如需了解详情,请参阅 IAM 权限。
拒绝原因
当 policyEvaluationRecord.decision 为 DENY 时,policyEvaluationRecord.denyReason 会说明原因。下表介绍了每种拒绝原因:
| 拒绝原因 | 时间 | 确定政策 |
|---|---|---|
FORBID_RULE_MATCHED |
至少有一项强制执行的 forbid 政策匹配。 |
匹配的 forbid 项政策。 |
NO_PERMIT_RULE_MATCHED |
载荷已解码,没有 forbid 政策匹配,没有政策评估失败,也没有 permit 政策匹配。 |
无 |
EVALUATION_ERROR |
没有匹配的 permit 或 forbid 政策,并且至少有一项政策未能评估,例如因为它读取了上下文中不存在的可选属性。 |
无。evaluationErrors 列出了错误。 |
UNRECOGNIZED_PAYLOAD |
实例无法解码 Transaction.payload 或将其投影到上下文中,例如,由于字节格式错误、指令或合约操作不受支持,或者比特币交易有多个收款人。不评估任何政策。 |
无。evaluationErrors 列出了该解析器错误。 |
原因按以下顺序确定:
- 如果无法解码载荷或将其投影到上下文中,则决策为
DENY,原因为UNRECOGNIZED_PAYLOAD。 - 否则,系统会评估每项强制执行的政策。如果任何
forbid政策匹配,则决策为DENY,原因为FORBID_RULE_MATCHED,即使permit政策也匹配或无法评估其他政策。 - 否则,如果至少有一项
permit政策匹配,则决策为PERMIT。其他政策中的错误仍会记录在evaluationErrors中。 - 否则,如果任何政策未能评估,则决策为
DENY,并显示EVALUATION_ERROR;如果每个政策都经过评估但未匹配,则决策为NO_PERMIT_RULE_MATCHED。
即使决策为 PERMIT,系统也会记录错误,因此请检查评估记录,找出因始终失败而从未匹配的政策。
场景
在场景 1 到 9 中,Alice (User::"alice@example.com") 创建了一个有效的以太坊转账,其中 action 设置为 Action::"internal_transfer_transaction",并包含以下元数据,除非场景另有说明:
"metadata": {
"fiatAmount": {"currencyCode": "USD", "units": "5000"},
"principalValue1": "treasury-desk"
}
该交易没有 validity,也没有 principalValue2。
下表总结了每种场景:
| 场景 | 强制执行的政策或载荷 | 决定 | 拒绝原因 |
|---|---|---|---|
| 1 | 无 | DENY |
NO_PERMIT_RULE_MATCHED |
| 2 | 一个匹配的permit |
PERMIT |
|
| 3 | 1 个不匹配的permit |
DENY |
NO_PERMIT_RULE_MATCHED |
| 4 | 两个 permit,一个匹配 |
PERMIT |
|
| 5 | permit 和 forbid 均匹配 |
DENY |
FORBID_RULE_MATCHED |
| 6 | 读取缺失属性的 permit |
DENY |
EVALUATION_ERROR |
| 7 | permit,has |
DENY |
NO_PERMIT_RULE_MATCHED |
| 8 | 读取缺失属性的 forbid,以及匹配的 permit |
PERMIT |
|
| 9 | 需要属性的 forbid |
DENY |
FORBID_RULE_MATCHED |
| 10 | 无法解码的载荷 | DENY |
UNRECOGNIZED_PAYLOAD |
场景 1:没有强制执行的政策
该实例没有 ACTIVE 或 PENDING_DISABLED 政策。
决策:DENY、NO_PERMIT_RULE_MATCHED。如果没有匹配的 permit 政策,每笔交易都会被拒绝。在新实例中,在政策作者激活 permit 政策之前,系统不会自动对任何交易进行签名;签名者必须对每笔交易进行授权。
场景 2:一个匹配的许可
permit (principal, action, resource)
when { context has fiat_amount && context.fiat_amount <= 10000 };
决定:PERMIT。金额 5,000 不超过 10,000。
determiningPolicyIds 列出了此政策。
场景 3:一个许可证不匹配
permit (principal, action, resource)
when { context has fiat_amount && context.fiat_amount <= 1000 };
决策:DENY、NO_PERMIT_RULE_MATCHED。金额 5,000 大于 1,000,并且没有其他政策允许该交易。
场景 4:两个许可,一个匹配
政策 A:
permit (principal, action, resource)
when { context has fiat_amount && context.fiat_amount <= 1000 };
政策 B:
permit (
principal == User::"alice@example.com",
action == Action::"internal_transfer_transaction",
resource == Resource::"transaction_management"
)
when {
context has fiat_amount && context.fiat_amount <= 25000 &&
context has principal_value1 && context.principal_value1 == "treasury-desk"
};
决定:PERMIT。政策 A 不匹配,但政策 B 匹配。只需一项匹配的 permit 政策即可。determiningPolicyIds 仅列出政策 B。
场景 5:禁止规则会覆盖允许规则
政策 A:
permit (principal, action, resource)
when { context has fiat_amount && context.fiat_amount <= 10000 };
政策 B:
forbid (principal, action, resource)
when { context has principal_value1 && context.principal_value1 == "treasury-desk" };
决策:DENY、FORBID_RULE_MATCHED。这两项政策都匹配,forbid 胜出。determiningPolicyIds 仅列出政策 B。
场景 6:许可读取缺失的属性
permit (principal, action, resource)
when { context.validity <= 3600 };
决策:DENY、EVALUATION_ERROR。该事务没有 validity,因此 context.validity 不存在,并且政策无法评估。没有其他政策允许该事务。evaluationErrors 列出了错误。
场景 7:同一许可证,使用 has
permit (principal, action, resource)
when { context has validity && context.validity <= 3600 };
决策:DENY、NO_PERMIT_RULE_MATCHED。context has validity 为 false,因此政策的计算结果为 false,而不是失败。该事务仍会被拒绝,但原因会显示政策已评估且不匹配。
场景 8:禁止读取缺失的属性
政策 A:
permit (principal, action, resource)
when { context has fiat_amount && context.fiat_amount <= 10000 };
政策 B:
forbid (principal, action, resource)
when { context.principal_value2 == "sanctioned-counterparty" };
决定:PERMIT。该交易没有 principalValue2,因此政策 B 无法评估并被跳过。政策 A 匹配。determiningPolicyIds 列出了政策 A,evaluationErrors 列出了政策 B 的错误。
场景 9:需要属性的禁止规则
政策 A 是场景 8 中的permit政策。政策 B:
forbid (principal, action, resource)
unless { context has fiat_amount };
决策:在这些场景中,事务的 PERMIT 具有 fiatAmount。如果 Alice 省略了 fiatAmount,则判定结果为 DENY,FORBID_RULE_MATCHED:context has fiat_amount 为 false,因此 forbid 政策匹配。此政策不会评估失败,因为它仅使用 has。
如需在一条 forbid 政策中将必需属性与其值检查结合使用,请先检查该属性:
forbid (principal, action, resource)
unless { context has fiat_amount && context.fiat_amount <= 10000 };
场景 10:无法解码的载荷
Alice 创建了一个交易,其 payload 无法由 Transaction.schema 的解析器解码,例如截断的 RLP 字节、具有多个接收者的比特币 PSBT 或具有不受支持指令的 Solana 交易。
决策:DENY、UNRECOGNIZED_PAYLOAD。不评估任何政策。
evaluationErrors 列出解析器错误,determiningPolicyIds 为空。
评估记录
每次评估都会生成一个无法更改的 PolicyEvaluationRecord。它在 Transaction.policyEvaluationRecord 中返回,并具有以下字段:
| 字段 | 值 |
|---|---|
decision |
PERMIT 或 DENY。 |
denyReason |
拒绝原因之一。当决策为 PERMIT 时,此字段未设置。 |
determiningPolicyIds |
在 PERMIT 上,匹配的 permit 政策的 ID。在 FORBID_RULE_MATCHED 上,匹配的 forbid 政策的 ID。否则为空。 |
evaluationErrors |
评估失败的政策中的错误,或 denyReason 为 UNRECOGNIZED_PAYLOAD 时的解析器错误。 |
evaluationContext |
用于评估政策的 Cedar 上下文,以 google.protobuf.Struct 形式表示。64 位整数存储为十进制字符串,以保持其精度。 |
policySetVersion |
评估所用的政策集版本。 |
keyListMemberships |
在评估期间解析的 KeyList 成员资格。 |
intentDigest |
所评估交易载荷的小写十六进制编码摘要。哈希取决于载荷的架构:ETHEREUM_TX_V2 的交易 Keccak-256、BITCOIN_TX_V2 的第一个输入的 sighash、SOLANA_TX_V1 的未签名消息 SHA-256,以及 EIP712_TYPED_DATA_V1 的 EIP-712 签名摘要 keccak256(0x1901 ++ domainSeparator ++ hashStruct(message))。 |
evaluationTime |
评估运行的时间。 |