OByte自治代理编程指南(二):Oscript语言
简介
自治代理(Autonomous Agents, AA)是DAG账本上不属于任何人的特殊地址(帐户)。自治代理仅对发送给它的触发交易进行响应,并且严格地按照自治代理的预定程序来执行。自治代理的程序代码是公开的,它用于实现自治代理对触发交易的响应。自治代理的响应取决于:
- 触发交易发送的金额:发送了多少资产;
- 触发交易发送的数据;
- 当收到触发交易时DAG账本的状态:包括余额、预言机发布的数据、认证、状态变量等。
自治代理的响应可以是以下一种或几种的组合:
- 将资产发回给发送方或第三方(可以是另一个自治代理);
- 通过发布数据、认证、更新状态变量等方式来更改DAG账本的状态。
自治代理的行为类似于自动售货机:接受投币以及用户在键盘上输入的数据(触发动作),并以出售一杯咖啡、播放一首歌或者做预设的任何事情作为响应。它们之间的共同之处在于:行为是可预测的,且是事先已知的。
自治代理没有关联的私钥/公钥。它们的交易没有签名。它们的交易由所有的全节点创建,只需遵循协议规则并执行自治代理的代码即可。所有节点总是得到相同的结果,并产生完全相同的响应交易。正如人们对DAG的期望一样,所有节点只需遵循规则即可到达相同的DAG账本状态,无需任何投票、计算证明或中心节点。
由于所有节点都将生成相同的自治代理响应交易,因此自治代理生成的响应交易不向全网广播。
自治代理的编程语言为领域特定语言Oscript。它是专门为自治代理设计的,可以轻松地编写自治代理程序,来清晰地描述自治代理需要完成的事情,并且不会产生难以跟踪的错误。
为了控制资源消耗,Oscript语言不支持循环。只有程序复杂性不超过上限的自治代理可以部署,资源消耗过多的程序不允许执行。通常,设定的资源上限能够满足绝大多数实际应用。
自治代理可以通过发送交易来调用其它自治代理。调用前要求调用方自治代理中的所有脚本都已执行完成,并且在将控制权传递给下一个自治代理之前所有更改都已提交,这可以避免其他智能合约平台中常见的重入问题。
定义
自治代理的地址遵循与所有其他OByte地址相同的规则:它们的定义是双元素数组,地址是该数组base32编码的校验和哈希。
自治代理地址定义为:
["autonomous agent", {
// here goes the AA code
}]
上述数组的第二个元素是一个对象,它是自治代理创建响应交易的模板。模板的结构和常规交易的结构类似,但是其中一些元素可以根据于输入和状态参数动态变化。动态元素使用特殊标记指定,并包含域特定语言Oscript代码:
{address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - 1000}"}
这个概念类似于PHP、ASP和JSP等语言的工作方式。例如,PHP脚本是一个混合HTML文件,其中散布着包含在<?php
和?>
标记之间的PHP代码片段。这些片段使HTML页面可以动态变化,同时保持其HTML结构。虽然当使用PHP生成HTML代码时,混合式HTML页面逐渐变得复杂,但通过在HTML中插入一小段代码来创建动态网页的难易程度是这些编程语言在网络发展初期的主要卖点之一。它们实现了快速原型设计、迭代、实验,并最终形成了一些现代互联网的组成(wordpress、facebook等)。
OByte中的交易单元是分布式账本的基本构成单位。它们通常表示为JSON对象,这和Web上HTML页面的相对应。 现在,为了便于创建表示交易的动态参数化JSON对象,我们允许将一些代码注入JSON并由开发人员来完成剩下的工作。
格式 | 脚本语言 | |
---|---|---|
Web | HTML | PHP |
OByte | JSON | Oscript |
下面是一个自治代理的定义示例 :
['autonomous agent', {
bounce_fees: { base: 10000 },
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - 1000}"}
]
}
}
]
}]
这里的messages
是自治代理响应交易的模板。它只有一条支付类型(payment
)消息,它将收到的金额(减去1000字节手续费)发送给发送方。用大括号{}
括起来的字符串中有代码片段,需要对它们进行动态求值,并将最终结果插入。
在发送生成的响应交易之前,还需要添加其他必要字段,并将该响应交易追加到DAG上,包括父单元、最后稳定单元、作者、时间戳、费用等等。
下面具体介绍自治代理响应交易模板格式的规则。
JSON格式
bounce_fees
{
bounce_fees: { base: 10000, "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=": 100 },
...
}
这是响应交易模板的可选字段,用于指定自治代理执行失败时从发送方收取的费用。在这种情况下,收到的所有资金都会自动退回给发送方,并减去bounce_fees
。费用以资产ID来标识(基础资产采用base
)。
基础资产的默认bounce_fees
是10000 bytes。所有其它资产的最低和默认bounce_fees
为0。非基础资产的bounce_fees
仅适用于自治代理实际收到的那些资产。
向自治代理发送任何低于bounce_fees
的触发交易都将导致无响应,自治代理会直接吞掉相关的资产。但是,此规则仅适用于从常规地址发送的资金。当从另一个自治代理收到资金时,不会检查bounce_fees
。
bounce_fees
字段会从最终生成的响应交易中删除。
messages
messages
是自治代理定义的主要部分。它为要生成的响应交易指定消息模板,并使用Oscript代码对消息模板进行参数化。
messages
可以是Obyte支持的任何消息类型(app
)。最常见的消息类型是payment
,它用于将任何资产的付款发送回发送方或第三方。其它消息类型包括:
asset
:用于定义新资产;data
:用于发送数据,包括向其它自治代理发送数据参数;data_feed
:用于发送数据预报,此时自治代理币变为一个预言机;profile
:用于发送自己的个人资料,自治代理向全网公布自己的身份;text
:用于将任意文本保存到DAG;definition
:用于发布新的自治代理;asset_attestors
:用于更改此自治代理先前定义的资产的证明者列表;attestation
:用于发布有关其它地址的信息,此时自治代理成为认证者;definition_template
:用于发布智能合约定义的模板;poll
:用于创建调查;vote
:用于投票,每个自治代理都有投票权。
还有一个特殊的消息类型为state
,它在常规交易中是不可用的,仅用于自治代理记录状态变量。后面还会有更多关于状态变量的介绍。
Oscript脚本
消息模板中的任何字符串、数字或布尔值都可以通过Oscript脚本计算。Oscript脚本执行后,其结果插入代替该脚本。例如,如果从地址2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7
向自治代理发送20000 bytes,则下面的脚本
{address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - 1000}"}
将会被替换为:
{address: "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7", amount: 19000}
对象键值也可以利用Oscript脚本进行参数化:
{
"{trigger.data.key}": "value"
}
下面将详细介绍Oscript脚本的详细规则。
cases
JSON包含标量(字符串、数字、布尔值)和对象(对象、数组)。标量可以直接通过Oscript脚本参数化。而对象的参数化方式有所不同。它们需要在消息模板中设置多个选项,并基于输入参数和状态选择其中一个。使用cases
来选择:
{
messages: {
cases: [
{
if: "{trigger.data.define}",
messages: [
// first version of messages
....
]
},
{
if: "{trigger.data.issue}",
init: `{
$amount = trigger.output[[asset=base]];
}`,
messages: [
// second version of messages
....
]
},
{
messages: [
// default version of messages
....
]
}
]
}
}
消息模板中对象/数组的值将替换为数组cases
。cases
数组的每个元素都是一个最多包含3个元素的对象:
if
:一个Oscript表达式。如果其值为真,则选择此选项,且其它选择则直接跳过。除了最后一个选项之外,所有选项都需要有if
。如果所有之前选项都为假,而最后一个选项没有if
,则选择最后一个选项;init
:一个可选的仅包含语句的Oscript脚本,如果相应的选项被选择,则在if
后立即执行;- 一个与原始对象/数组命名相同的元素(上例中为
messages
)。如果相应的选项被选择,则原始字段将替换为此元素的值。
在上面的示例中,如果选择了第二种情况,原始对象将变为:
{
messages: [
// second version of messages
....
]
}
cases
可以嵌套使用。cases
可以用于messages
中的任何非标量元素,而不仅仅是messages
。
条件对象
与上述情况类似,任何对象都可以有一个额外的if
字段。如果值为假,则从对象或数组中移除整个对象。在这种情况下,它的内部Oscript脚本也不会被执行。
{
messages: [
{
app: 'data',
payload: {
timestamp: `{timestamp}`,
subscriber: `{trigger.address}`
}
},
{
if: `{trigger.data.withdrawal_amount > 0}`,
app: 'payment',
payload: {
asset: "base",
outputs: [
{address: "{trigger.address}", amount: "{trigger.data.withdrawal_amount}"}
]
}
}
]
}
在上面的例子中,payment
消息只有在trigger.data.withdrawal_amount
大于0的时候才生成。if
字段在最终的响应交易中会被删除。
init脚本
与上述情况类似,任何对象都可以有一个额外的init
脚本。如果if
字段存在且为真,则立即执行init
脚本。如果没有if
字段,则立即执行init
脚本。
init
脚本必须是一个仅包含语句的Oscript脚本,它没有返回值。init
脚本在最终的响应交易中会被删除。
{
messages: [
{
init: `{ $addr = trigger.address; }`,
app: 'data',
payload: {
timestamp: `{timestamp}`,
subscriber: `{$addr}`
}
},
{
if: `{trigger.data.withdrawal_amount > 1000}`,
init: `{ $amount = trigger.data.withdrawal_amount - 1000; }`,
app: 'payment',
payload: {
asset: "base",
outputs: [
{address: "{trigger.address}", amount: "{$amount}"}
]
}
}
]
}
对象字段和数组元素
如果任何对象字段或数组元素的值计算为空字符串,则删除此字段或元素。
例如,对象
{
field1: `{ (1 == 2) ? "value1" : "" }`,
field2: "value2"
}
将变为
{
field2: "value2"
}
数组
[ `{ (1 == 2) ? "value1" : "" }`, "value2" ]
将变为
[ "value2" ]
发送所有资产
如果payment
类型消息中的输出中的amount
字段被省略或为空字符串(这导致按照上述规则将其删除),则此输出将发送自治代理所有剩余的金额。
{
messages: [
{
if: `{trigger.data.send_all}`,
app: 'payment',
payload: {
asset: "base",
outputs: [
{address: "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7", amount: 1000},
{address: "{trigger.address}"}
]
}
}
]
}
在上面的例子中,1000 bytes被发送到2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7
,自治代理的剩余余额(以bytes为单位)被发送到触发自治代理的地址。
空对象和数组
如果对象或数组由于删除而变空,则它也会从最终的响应消息中删除。
状态消息
状态消息是messages
数组中执行状态更改的特殊消息。它是唯一分配状态变量的Oscript脚本。与始终具有有效负载的常规消息不同,状态消息的消息类型为state
,且包含状态更改脚本:
{
messages: [
{
app: 'payment',
payload: {
asset: 'base',
outputs: [
{address: "{trigger.address}", amount: "{trigger.output[[asset=base]] - 1000}"}
]
}
},
{
app: 'state',
state: `{
var['responded'] = 1;
var['total_balance_sent_back'] += trigger.output[[asset=base]] - 1000;
var[trigger.address || '_response_unit'] = response_unit;
}`
}
]
}
状态消息必须始终是messages
数组中的最后一条消息。 它不包含在最终响应交易中,并且在响应交易已准备好之后执行其脚本(状态脚本)。它是状态变量可用的唯一Oscript脚本。 状态脚本只包含语句,不允许返回任何值。
次级自治代理
生成的响应交易单元可以包含到其它自治代理的输出。 在这种情况下,响应交易会触发次级自治代理。次级自治代理从消息中接收上一级自治代理的输出和数据。
次级自治代理与一般自治代理行为相似,只是它们可能会响应低于最低bounce_fees
费用的交易。
如果自治代理触发了多个次级自治代理,则会以确定的顺序处理它们,以确保结果在所有节点上都可重现。如果次级自治代理触发了下一级自治代理,则在进入下一个次级自治代理之前处理它。
如果其中任何自治代理失败,则所有的自治代理都将失败,产生的所有更改都会回滚。
次级自治代理的总数不能超过10,否则自治代理执行失败并回滚。
失败
如果自治代理由于任何原因执行失败(错误的公式、尝试发送无效的回复、尝试发送比多余余额的资产等),它会尝试将所有收到的资产退回给发送方,需要减去退回费用,并且所有状态更改都会回滚。
如果退回交易创建也失败,则不会有任何响应交易,即自治代理吞掉所有资产。
未保存到分布式账本的响应交易(请参阅下一节)将包含一条错误消息,并说明失败的原因。
响应交易
当触发交易单元达到稳定时,自治代理被激活并产生响应。如果触发交易单元触发多个自治代理或者多个触发交易单元同时稳定,则以确定的顺序处理触发的自治代理以确保所有节点上的再现性。
响应交易单元应该有一个或两个父单元:
- 刚刚稳定的主链交易单元,该单元包含触发交易单元;
- 自治代理生成的相应交易单元(由任何自治代理生成,而不仅仅是当前的自治代理生成的),如果它没有包含在第一个父单元中。
任何后续响应交易(由次级自治代理生成并响应同一主链序号的其它触发交易)在第一个响应交易单元之后链接,然后一个接一个地链接。
每次响应后,都会发出4个事件:
aa_response
aa_response_to_unit-
+ trigger_unitaa_response_to_address-
+ trigger_addressaa_response_from_aa-
+ aa_address
全节点的应用程序可以订阅这些事件以接收有关他们的响应信息,例如:
eventBus.on('aa_response_to_address-' + '2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7', (objAAResponse) => {
// handle event
})
轻节点的应用程序也可以将这些地址添加到观察列表从而订阅这些时间信息:
var aa_address = '';
const walletGeneral = require('ocore/wallet_general.js');
const eventBus = require('ocore/event_bus.js');
walletGeneral.addWatchedAddress(aa_address, () => {
eventBus.on('aa_response_from_aa-' + aa_address, (objAAResponse) => {
// handle event
});
});
所有4个事件处理程序都将objAAResponse
对象作为单个参数接收:
{
mci: 2385,
trigger_address: '2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7',
trigger_unit: 'f2S6Q3ufjzDyl9YcB51JUj2z9nE1sL4XL2VoYOrVRgQ=',
aa_address: 'JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY',
bounced: false,
response_unit: 'JCJ1ZGkl2BtUlYoeu6U2yshp97pen/fIkTHvaKYjZa4=',
objResponseUnit: {
version: '2.0dev',
alt: '3',
timestamp: 1560939440,
messages: [ ... ],
authors: [ ... ],
last_ball_unit: 'cxjDfHzWWgW8LqsC8yoDhgCYmwThmFfkdygGnDrxiFg=',
last_ball: 'gjg8W2cE4WGFAIIsU2BvLOQKOpH/03Oo1SDS3/2SQDs=',
witness_list_unit: '3gLI9EnI2xe3WJVPwRg8s4CB24ruetuddS0wYa2EI3c=',
parent_units: [ 'f2S6Q3ufjzDyl9YcB51JUj2z9nE1sL4XL2VoYOrVRgQ=' ],
headers_commission: 267,
payload_commission: 157,
unit: 'JCJ1ZGkl2BtUlYoeu6U2yshp97pen/fIkTHvaKYjZa4='
},
response: {}
}
该对象具有以下字段:
mci
:触发交易单元的主链序号;trigger_address
:发送出发交易单元的地址;aa_address
:自治代理地址;bounced
:如果出发交易被退回为true
,否则为false
;response_unit
:响应交易单元的哈希值,如果没有响应则为null
;objResponseUnit
:响应交易单元对象,如果没有响应则为null
;response
:来自脚本的响应,该对象最多可以包含两个字段:错误消息error
和脚本设置的响应变量responseVars
。
复杂度
与其它智能合约定义一样,自治代理具有复杂度上限,不能超过100。某些操作涉及复杂的计算或对数据库的访问,这些操作会被计算并添加到总复杂性计算中。其它操作(如+
、 -
等)不会增加复杂性。下面会给出哪些操作计入复杂性。
总复杂性是所有Oscript的复杂性的总和。它仅在部署期间计算和检查,且包括所有分支的复杂性,即使它们中的一些可能在运行时未被激活。
如果复杂度超过100,则验证失败并且无法部署自治代理。
部署
在使用之前,必须通过发送包含app=definition
的消息单元来部署自治代理代码。该消息在其有效负载中具有自治代理的定义。
{
app: 'definition',
payload_location: 'inline',
payload: {
address: "JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY",
definition: ["autonomous agent", {
...
}]
},
...
}
两次发布相同的定义不会造成错误。
除了自治代理代码中直接指定的用户之外,部署自治代理的用户不具有任何特权。
部署后,自治代理的定义永远不会改变。
在自治代理的定义单元之前发送到自治代理地址的任何交易都会被视为常规交易(指的是那些主链序号在自治代理定义单元之前的那些交易),它们只是增加地址的余额但不触发任何动作。这可能是在自治代理实际启动之前给自治代理充值的快捷方式。
两种类型Oscript
可以在自治代理中使用两种类型的Oscript脚本:
- 仅包含语句的脚本(仅包含赋值),不返回任何值,init脚本和状态消息是必须是仅包含语句的脚本;
- 具有返回值的脚本,它们可以包含多个表达式语句,但最后一个计算表达式的结果是脚本的结果。
仅包含语句脚本的示例:
$amount = trigger.output[[asset=base]];
$action = trigger.data.action;
具有返回值的脚本示例:
$amount = trigger.output[[asset=base]];
$action = trigger.data.action;
$amount*2
最后一个表达式$amount*2
将作为返回值。
非标量返回值
通常,Oscript脚本的返回值是标量:字符串,数字或布尔值,用于插入原来Oscript的位置。
类似地,如果返回值是一个对象,它将被展开并插入代替原始的Oscript。这可以用于通过trigger.data
发送准备好的对象。
例如,如果trigger.data
是
{
output: {address: 'BSPVULUCOVCNXQERIHIBUDLD7TIBIUHU', amount: 2e5}
}
自治代理具有如下消息:
{
app: 'payment',
payload: {
asset: "base",
outputs: [
`{trigger.data.output}`
]
}
}
则最终的响应消息为:
{
app: 'payment',
payload: {
asset: "base",
outputs: [
{address: 'BSPVULUCOVCNXQERIHIBUDLD7TIBIUHU', amount: 2e5}
]
}
}
运算符
数学运算符 +, -, *, /, %, ^
$amount - 1000
$amount^2 / 4
操作数是数字或者可强制转为数字的数据。
对于幂运算^
,有一些额外的规则:
e^x
会以e
的精确值进行计算,而不会进行舍入;- 计算结果超过
MAX_SAFE_INTEGER
的会报错; - 对于非整数幂运算,计算按照
x^y=e^(y * ln(x))
进行,并对中间结果进行舍入,虽然有精度损失,但是可以保证可重现的结果; - 采用幂运算会使得计算复杂度+1。
字符串连接符 ||
'abc' || 'def' -> 'abcdef'
非字符串的类型将转换为字符串进行操作。
二元逻辑运算符 AND, OR
小写字母的and
和or
也是允许的。非布尔操作数将转换为布尔值。结果是布尔值。如果第一个操作数的计算结果为true
,则不计算OR
的第二个操作数。如果第一个操作数的计算结果为false
,则不计算AND
的第二个操作数。
一元逻辑运算符 NOT
小写字母的not
也是允许的,也可以写作!
。非布尔操作数将转换为布尔值。结果是布尔值。
条件运算符 OTHERWISE
小写字母的otherwise
也是允许的。
expr1 OTHERWISE expr2
如果expr1
为真,则返回其结果并且不计算expr2
。否则,将计算expr2
并返回其结果。
比较运算符 ==, !=, >, >=, <, <=
如果两个操作数都是布尔值,或者两个操作数都是数字,则结果很简单。如果两个操作数都是字符串,则按字典顺序进行比较。如果两个操作数都是对象,则只允许==
和!=
,其他运算符将导致错误。
如果操作数是不同类型的:
- 如果它们中的任何一个是对象或布尔值,它会导致错误,
- 如果它们中的任何一个是字符串,则只允许使用
==
和!=
,并且在比较之前将非字符串操作数转换为字符串,其他运算符将导致错误, - 所有其他类型的组合都会导致错误。
三元运算符 ?
condition ? expr1 : expr2
如果condition
为真,则计算并返回expr1
,否则计算并返回expr2
。
常量
pi
常量pi保留15位数字精度:3.14159265358979。
e
欧拉常数保留15位数字精度:2.71828182845905。
内建函数
平方根sqrt和自然对数ln
sqrt(number)
ln(number)
使用这些函数会使得计算复杂度+1。负数会导致错误。非数字输入转换为数字或导致错误。
绝对值 abs
abs(number)
返回数字的绝对值。非数字输入转换为数字或导致错误。
舍入 round, ceil, floor
round(number [, decimal_places])
ceil(number [, decimal_places])
floor(number [, decimal_places])
将输入数字舍入到指定的小数位数(如果省略则为0)。round
使用ROUND_HALF_EVEN
规则。非数字输入转换为数字或导致错误。负整数或非整数使用decimal_places
会导致错误。decimal_places
大于15会导致错误。
最小值和最大值 min, max
min(number1, [number2[, number3[, ...]]])
max(number1, [number2[, number3[, ...]]])
返回该组数字中的最小值或最大值。非数字输入转换为数字或导致错误。
平方和的平方根 hypot
hypot(number1, [number2[, number3[, ...]]])
返回所有参数的平方和的平方根。布尔参数转换为1
和0
,对象取为1
,所有其他类型都会导致错误。即使某些中间结果(平方和)溢出,该函数也会返回非无穷大的结果。使用此函数使计算复杂度+1。
JSON字符串解析: json_parse
json_parse(string)
尝试解析输入的JSON字符串。如果解析的结果是对象,则返回该对象。如果结果是标量(布尔值,字符串,数字),则返回标量。使用此函数使得计算复杂度+1。如果解析失败,则返回false。非字符串输入将转换为字符串。
将对象序列化为JSON:json_stringify
json_stringify(object)
将输入参数字符串化为JSON。参数也可以是数字,布尔值或字符串。如果它是IEEE754范围之外的数字,则计算将失败。返回的JSON中的对象按键排序。
number_from_seed
number_from_seed(string)
number_from_seed(string, max)
number_from_seed(string, min, max)
从种子字符串生成数字。相同的种子总是产生相同的数字。从不同种子字符串生成的数字在指定的间隔内均匀分布。
- 第一种形式返回从
0
到1
的小数。 - 第二种形式返回从
0
到最大的整数。 - 第三种形式返回从
min
到max
的整数。
此函数对于从种子字符串生成伪随机数很有用。它使得计算复杂性+1。
sha256
sha256(string)
返回输入字符串sha256的base64编码。非字符串输入转换为字符串。此函数使得计算复杂度+1。
is_valid_signed_package
is_valid_signed_package(signedPackage, address)
如果signedPackage
对象是由地址签名的有效签名包,则返回true
,否则返回false
(即使signedPackage
的格式不正确,计算也不会失败)。address
必须是有效地址,否则计算将失败并显示错误。此函数使得计算复杂度+1。
signedPackage
对象通常通过触发交易传递,并具有以下结构:
{
"signed_message": {
"field1": "value1",
"field2": "value2",
...
},
"authors": [
{
"address": "2QHG44PZLJWD2H7C5ZIWH4NZZVB6QCC7",
"authentifiers": {
"r": "MFZ0eFJeLAgAmm6BJdvbEzNt7x0H2Fb5RQBBpMSmyVFMLM2r2SX5chU9hbEWXExkz/T2hXAk1qHmxkAbbpZw8w=="
}
}
],
"last_ball_unit": "izgjyn9bpbJjwpKQV7my0Dq1VUHbzrLpWLrdR0fDydw=",
"version": "2.0"
}
其中,
signed_message
是要签名的消息,它可以是对象,数组或标量;authors
是一组签名消息的作者(通常是一个),它与交易单元中的authors
具有相同的结构,包括签名地址,认证(通常是签名)和可选的定义;last_ball_unit
:最近稳定交易单元,指示DAG上签名消息的位置。如果authors
中未包含定义,则必须在DAG账本历史记录中知道该定义。 如果signedPackage
中没有last_ball_unit
,则authors
中每个地址都必须包含其定义;version
:总是2.0
。
通常,signedPackage
是通过从signed_message
模块调用signMessage
函数创建的:
var headlessWallet = require('headless-obyte');
var signed_message = require('ocore/signed_message.js');
signed_message.signMessage(message, address, headlessWallet.signer, true, function (err, signedPackage) {
// handle result here
trigger.data.signedPackage = signedPackage;
});
该函数创建一个正确结构的signedPackage
对象,可以将其添加到trigger.data
。
is_valid_sig
is_valid_sig(message, public_key, signature)
如果签名是与public_key
对应的私钥的正确ECDSA
签名,则返回true
,否则返回false
。
message
是与正在签名的消息对应的字符串,该函数将在验证签名之前使用SHA-256
对消息进行哈希。如果消息不是字符串,则计算将失败。
public_key
是包含PEM格式的公钥的字符串。例如:
-----BEGIN PUBLIC KEY-----
MEowFAYHKoZIzj0CAQYJKyQDAwIIAQEEAzIABG7FrdP/Kqv8MZ4A097cEz0VuG1P\n\ebtdiWNfmIvnMC3quUpg3XQal7okD8HuqcuQCg==
-----END PUBLIC KEY-----
----- BEGIN PUBLIC KEY -----
和----- END PUBLIC KEY -----
可以省略,空格或回车将被忽略。如果public_key
不是字符串,不是支持的加密方式,或者没有所需的长度,则计算将失败。
signature
是一个包含base64
或十六进制格式的签名的字符串。如果签名不是字符串或不是base64
或十六进制格式,则计算将失败。
支持的加密方式(第一个pubkey
字符):brainpoolP160r1,brainpoolP160t1,brainpoolP192r1,brainpoolP192t1,brainpoolP224r1,brainpoolP224t1,brainpoolP256r1,brainpoolP256t1,prime192v1,prime192v2,prime192v3,prime239v1,prime239v2,prime239v3,prime256v1,secp112r1,secp112r2,secp128r1,secp128r2,secp160k1,secp160r1,secp160r2,secp192k1,secp224k1,secp224r1, secp256k1,secp384r1,sect113r1,sect113r2,sect131r1,sect131r2,wap-wsg-idm-ecid-wtls1,wap-wsg-idm-ecid-wtls4,wap-wsg-idm-ecid-wtls6,wap-wsg-idm-ecid- wtls7,wap-wsg-idm-ecid-wtls8,wap-wsg-idm-ecid-wtls9。
bounce
bounce(string);
中止脚本的执行,并将错误消息作为函数的参数传递。 收到的资金将退回给发送方(减去退回费用)。
数据转换
字符串转换
有些函数或操作数需要以字符串作为输入,因此可能需要进行字符串转换:
- 数字使用十进制表示转换为字符串。对于指数大于或等于
21
或小于或等于-7
的数字,使用指数表示字符串。 - 布尔值转换为字符串
true
和false
。 - 对象变为字符串
true
。
数字转换
有些函数或操作数需要以数字作为输入,因此需要将非数字类型转换为数字:
- 布尔值
true
和false
分别转换为数字1
和0
。 - 对象转换为数字
1
。 - 字符串转换报错。
布尔值转换
0
和空字符串变为false
,所有其他值变为true
。任何转换为true
的值都称为truthy
,否则称为falsy
。
流程控制
return
return expr;
中断脚本的执行并返回expr
的值。此语法只能在具有返回值的oscript中使用。
return;
中断脚本的执行而不返回任何值。此语法只能在仅有语句的oscript中使用:init
和state
。
if else
if (condition){
$x = 1;
$y = 2 * $x;
}
else{
$x = 2;
$z = $x^3;
}
如果condition
为真,则执行第一个语句块,否则执行第二个语句块。else
部分是可选的。
如果语句块只包含一个语句,则{}
中是可选的:
if (condition)
$x = 1;
注释
行注释:
// this is a comment line
$x = 1; // this part of line is a comment
同时也支持块注释:
/*
A comment block
*/
引用外部变量
trigger.address
向自治代理发送触发交易的地址。如果触发交易由多个地址签名,则使用第一个地址。
trigger.initial_address
当有多级自治代理时,这是第一级自治代理的触发交易的发送方地址。如果没有多级自治代理,则与trigger.address
相同。当自治代理向另一个自治代理发送交易时,trigger.initial_address
保持不变。
trigger.unit
发送到自治代理的触发交易。
trigger.output
trigger.output[[asset=assetName]].field
trigger.output[[asset!=assetName]].field
发送到自治代理的指定资产。assetName
可以是基础资产base
或任何资产ID。field
可以是amount
、asset
或省略。如果省略,则默认为金额。如果触发交易的同一资产中有多个输出,则将它们的金额相加。
搜索条件可以是=(asset = assetName)
或!=(asset!= assetName)
。
例子:
trigger.output[[asset=base]]
trigger.output[[asset=base]].amount
trigger.output[[asset="j52n7Bfec9jW"]]
trigger.output[[asset=$asset]]
trigger.output[[asset!=base]]
trigger.output[[asset!=base]].amount
if (trigger.output[[asset!=base]].asset == 'ambiguous'){
...
}
如果没有满足搜索条件的输出,则返回的.amount
为0,返回的.asset
为字符串none
。如有必要,您的代码应检查此字符串。
如果有多个输出满足搜索条件(只能用于!=
),则返回的.asset
是一个不明确的字符串。如有必要,您的代码应检查此字符串。此时,试图访问.amount
会导致执行失败。
trigger.data
在触发交易中发送的数据。trigger.data
返回整个数据对象,trigger.data.field1.field2
或trigger.data.field1[expr2]
可以访问更深层的嵌套字段:
- 如果是对象,则返回对象;
- 如果它是标量(字符串,数字或布尔值),则返回标量;
- 如果它不存在,则返回false。
例如,如果触发单元中有数据类型的消息
{
"app": "data",
"payload": {
"field1": {
"field2": "value2",
"abc": 88
},
"abc": "def"
},
"payload_hash": "..."
}
trigger.data
等于
{
"field1": {
"field2": "value2",
"abc": 88
},
"abc": "def"
}
trigger.data.field1
等于
{
"field2": "value2",
"abc": 88
}
trigger.data.field1.field2
等于字符串value2
,trigger.data.field1['a' || 'bc']
等于88,trigger.data.field1.nonexistent
等于布尔值false
,trigger.data.nonexistent.anotherfield
等于布尔值false
。
mci
触发交易单元的主链序号,这与响应交易连接的主链单元的主链序号相同。
timestamp
最近稳定的主链单元的时间戳,该单元的稳定触发了自治代理开始执行。这是与响应交易单元(如果有)相连的单元。
mc_unit
包含(或等于)触发单元的主链单元的哈希值。
this_address
自治代理的地址。
response_unit
自治单元的响应交易单元的哈希值。此变量仅在状态脚本中可用。任何其它脚本中对此变量的任何引用都将触发错误。
asset
asset[expr].field
asset[expr].[field_expr]
提取有关资产的信息。使用该表达式使得计算复杂度+1。expr是基础资产base
或计算为资产ID的表达式。
如果field
为on
,则field_expr
应为以下之一:
cap
:number
,资产的总供应量,对于无上限资产,返回0;is_private
:boolean
,资产是否私有;is_transferrable
:boolean
,是资产是否可转移;auto_destroy
:boolean
,当发送到发行方地址时,资产是否会被自动销毁;fixed_denominations
:boolean
,资产是否以固定面额发行;issued_by_definer_only
:boolean
,是否仅由发行方发布的资产;cosigned_by_definer
:`boolean
,每次转账是否都应该由发行方确认;spender_attested
:boolean
,是否每个持有人通过认证;is_issued
:boolean
,资产是否已发布。
示例:
asset[base].cap
asset["base"].cap
asset["n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY="].is_issued
asset["n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY="]['is_' || 'issued']
如果资产不存在,则任何字段均返回false
。
data_feed
data_feed[[oracles=listOfOracles, feed_name=nameOfDataFeed, ...]]
按搜索条件查找数据订阅,使用后计算复杂度+1。
[[ ]]
之间列出了多个搜索条件,它们的顺序无关紧要。
oracles
:string
,由:
分隔的oracle地址列表(通常只有一个oracle),this address
也是一个有效的oracle地址,它指向当前自治代理;feed_name
:string
,数据订阅的名称;feed_value
:string
或number
,可选,仅搜索具有该值的数据订阅;min_mci
:number
,可选,仅搜索指定的主链序号;ifseveral
:string
,可选,last
或abort
,如果发现多个匹配所有搜索条件的值,返回最后一个或中止脚本,默认为last
;ifnone
:string
或number
或boolean
,可选,如果没有找到则返回的值,默认情况下,这会导致错误并中止脚本;what
:string
,可选,value
或unit
,返回的内容,数据订阅的值或单元,默认值为value
;type
:string
,可选,auto
或string
,返回的类型,默认为auto
。对于auto
,有效IEEE754数字的数据值将作为数字返回,否则它们将作为字符串返回。如果是string
,则返回的值始终为字符串。此设置仅影响从数据库中提取的值。如果使用ifnone
,则始终保留ifnone
值的原始类型。
搜索的数据范围在触发交易单元的主链序号之前。如果有多个自治代理具有相同的主链序号,则也搜索先前的自治代理响应交易。
示例:
data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC', feed_name='BTC_USD']]
data_feed[[oracles=this address, feed_name='score']]
data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC:I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT', feed_name='timestamp']]
in_data_feed
in_data_feed[[oracles=listOfOracles, feed_name=nameOfDataFeed, feed_value>feedValue, ...]]
确定是否可以通过搜索条件找到数据订阅。返回true
或false
。这将使得计算复杂度+1。
双括号之间列出了多个搜索条件,它们的顺序无关紧要。
oracles
:string
,由:
分隔的oracle地址列表(通常只有一个oracle)。当前自治代理的地址也是一个有效的oracle地址;feed_name
:string
,数据订阅的名称;feed_value
:string
或number
,仅搜索=
、!=
、>
、>=
、<
或<=
超过指定值的那些数据;min_mci
:number
,可选,仅搜索指定的主链序号。
搜索的数据范围在触发交易单元的主链序号之前。如果有多个自治代理具有相同的主链序号,则也搜索先前的自治代理响应交易。
示例:
in_data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC', feed_name='BTC_USD', feed_value > 12345.67]]
in_data_feed[[oracles=this address, feed_name='score', feed_value=$score]]
in_data_feed[[oracles='JPQKPRI5FMTQRJF4ZZMYZYDQVRD55OTC:I2ADHGP4HL6J37NQAD73J7E5SKFIXJOT', feed_name='timestamp', feed_value>=1.5e9]]
attestation
attestation[[attestors=listOfAttestors, address=attestedAddress, ...]].field
attestation[[attestors=listOfAttestors, address=attestedAddress, ...]][field_expr]
按搜索条件查找用户认证。这将使得计算复杂度+1。
双括号之间列出了多个搜索条件,它们的顺序无关紧要。
attestors
:string
,由:
分隔的认证方地址列表(通常只有一个认证者),该自治代理地址也是有效的认证者地址;address
:string
,认证的地址;ifseveral
:string
,可选,last
或abort
,如果发现多个匹配所有搜索条件的值,返回最后一个或中止脚本,默认为last
;ifnone
:string
或number
或boolean
,可选,如果没有找到则返回的值,默认情况下,这会导致错误并中止脚本;type
:string
,可选,auto
或string
,返回的类型,默认为auto
。对于auto
,有效IEEE754数字的数据值将作为数字返回,否则它们将作为字符串返回。如果是string
,则返回的值始终为字符串。此设置仅影响从数据库中提取的值。如果使用ifnone
,则始终保留ifnone
值的原始类型。field string
或field_expr
表达式是可选的,它们表示应返回其值的用户认证字段。如果没有field
或field_expr
,则在找到用户认证时返回true
。
如果未找到匹配的用户认证,则返回ifnone
指定的值。如果没有ifnone
,则返回`false
。如果存在匹配的用户认证但请求的字段不存在,则结果就好像用户认证不存在一样。
搜索的数据范围在触发交易单元的主链序号之前。如果有多个自治代理具有相同的主链序号,则也搜索先前的自治代理响应交易。
示例:
attestation[[attestors='UOYYSPEE7UUW3KJAB5F4Y4AWMYMDDB4Y', address='BI2MNEVU4EFWL4WSBILFK7GGMVNS2Q3Q']].email
attestation[[attestors=this address, address=trigger.address]]
attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address='TSXOWBIK2HEBVWYTFE6AH3UEAVUR2FIF', ifnone='anonymous']].steem_username
attestation[[attestors='JEDZYC2HMGDBIDQKG3XSTXUSHMCBK725', address='TSXOWBIK2HEBVWYTFE6AH3UEAVUR2FIF']].reputation
balance
balance[asset]
balance[aa_address][asset]
返回指定资产中自治代理的余额。如果省略aa_address
,则假定是当前自治代理。asset
可以是代表bytes
的base
,或者任何其它资产的资产ID,或任何计算结果为资产ID或base
的表达式。这将使得计算复杂度+1。
返回的余额包括从当前触发交易接收到的。
示例:
balance[base]
balance["n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY="]
balance["JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY"][base]
input, output
output[[asset=assetID, amount>minAmount, address=outputAddress]].field
input[[asset=assetID, amount=amountValue, address=inputAddress]].field
尝试按搜索条件在当前交易单元中查找输入或输出。
双括号之间列出了多个搜索条件,它们的顺序无关紧要。 所有搜索条件都是可选的,但必须至少有一个。
asset
:string
,输入或输出的资产,可以是代表bytes
的base
,比较运算符只能是=
或!=
;address
:string
,接收输出或花费输入的地址,可以是this address
或other address
,比较运算符只能是=
或!=
;amount
:number
,输入或输出的数量,允许的比较运算符是=
、!=
、>
、>=
、<
、<=
。
field
是amount
,address
和asset
之一,它代表我们感兴趣的有关输入或输出的信息。
如果搜索条件未找到或存在多个匹配条目,则脚本将执行失败。
input[[asset=base]].amount
output[[asset = base, address=GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN]].amount
output[[asset = base, address="GFK3RDAPQLLNCMQEVGGD2KCPZTLSG3HN"]].amount
output[[asset = "n9y3VomFeWFeZZ2PcSEcmyBb/bI7kzZduBJigNetnkY=", amount>=100]].address
变量
局部变量
$name1 = 1;
$name2 = 'value';
${'name' || 3} = $name1 + 10;
局部变量名称始终以$
为前缀。如果需要计算变量名称本身,则表达式用大括号括起来。
局部变量仅在自治代理执行期间存在。每个变量只能分配一次值(单一分配规则)。
每个局部变量在分配后仅在其自己的Oscript中可见。如果它在if
块中分配,则它也可用于所有相邻和封闭的Oscript。如果它是在init
块中分配的,那么它也可以在所有相邻的Oscript中使用,除非在同一对象中包含的所有Oscript中。
[
{
if: `{
$amount = trigger.output[[asset=base]];
$amount == 10000 // the result of the last expression is the result of if
}`,
init: `{
$half_amount = round($amount / 2); // here we can reference $amount set in if
}`,
messages: [
{
app: 'payment',
payload: {
asset: "base",
outputs: [
{
address: "{ trigger.address }",
amount: `{
$half_amount // we are in an enclosed oscript and can reference $half_amount set in init
}`
}
]
}
},
{
app: 'state',
state: `{
var['received'] = $amount; // we are in an enclosed oscript and can reference $amount set in if
var['sent_back'] = $half_amount; // we are in an enclosed oscript and can reference $half_amount set in init
}`
}
]
},
{
if: `{trigger.data.payout}`,
init: `{
// here we cannot reference $amount nor $half_amount from the above
// we can even assign other values to them without breaking the single-assignment rule
$amount = 10;
}`,
...
}
]
如果引用了未分配的局部变量,则将其视为false
。
花括号中的if-else
块不会为局部变量创建单独的范围:
if (trigger.data.deposit){
$amount = trigger.output[[asset=base]];
}
$fee = round($amount * 0.01); // we can reference the $amount from above
局部变量可以包含任何类型的值:字符串,数字,布尔值或对象。当局部变量包含对象时,可以通过.
或[]
选择访问对象的各个字段:
$data = trigger.data;
$action = $data.params.action;
$player_score = $data.params[$player_name || '_score'];
如果指定的对象字段不存在,则将该值视为false
。
状态变量
状态变量在自治代理的调用之间保持不变。
访问状态变量:
var['var_name1']
var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name1']
分配状态变量:
var['var_name1'] = 'var_value';
var['var_name2'] = 10;
var['var_name3'] += 10;
var['var_name4'] = false;
var ['var_name']
读取当前自治代理存储的状态变量var_name
的值。
var['AA_ADDRESS']['var_name']
读取存储在自治代理AA_ADDRESS
下的状态变量var_name
的值。 AA_ADDRESS
用于指代当前自治代理。如果没有这样的变量,则返回false
。
状态变量可以在任何Oscript中访问,但只能在状态脚本中分配。只能分配当前自治代理的状态变量,其它自治代理的状态变量是只读的。状态变量可以多次重新分配,但只有最终值才会保存到数据库中,并且只有自治代理成功执行后。所有更改都以原子方式提交。如果自治代理执行失败,则会回滚对状态变量的所有更改。
状态变量可以暂时保存字符串,数字和布尔值,但是当持久化时,true
将转换为1,而false
会导致从存储中删除状态变量。
如果赋值的右侧是对象,则赋值为true
,状态变量无法存储对象。
除了常规赋值=
之外,还可以使用以下运算符修改状态变量:
+=
:增量;-=
:递减;*=
:乘以;/=
:除以;%=
:求余;||=
:字符串连接。
对于字符串连接,当前变量将转换为字符串。
对于+=
、-=
、*=
、/=
、%=
,现有的布尔值转换为1或0,而字符串导致错误。
如果变量在其中一个赋值之前不存在,则将其视为false
并相应地转换为数字或字符串。
对状态变量的每个读或写操作都会使计算复杂度+1。具有修改的分配也使计算复杂度+1。
示例:
var['sent_back'] = $half_amount;
var['count_investors'] += 1;
var['amount_owed'] += trigger.output[[asset=base]];
var['pending'] = false;
$x = var['JVUJQ7OPBJ7ZLZ57TTNFJIC3EW7AE2RY']['var_name1'];
响应变量
response['key'] = 'text';
向响应交易中添加变量。响应变量不影响状态,它们仅用于通知发送方和其他感兴趣的人关于自治代理执行的操作。
响应变量只能分配,但永远不会读取。可以在任何Oscript中多次分配和重新分配响应变量。它们可以包含类型的值:string
,number
,boolean
。使用对象进行赋值时将转换为true
。
示例:分配响应变量
response['message'] = "set exchange rate to 0.123 tokens/byte";
response['deposit'] = 2250000;
将会生成如下对象:
{
"responseVars": {
"message": "set exchange rate to 0.123 tokens/byte",
"deposit": 2250000
}
}
分号
每个变量赋值必须以分号;
结束。return
和bounce
语句也以分号结束。
$amount = trigger.output[[asset=base]];
var['sent_back'] = round($half_amount/2);
response['message'] = "set exchange rate to 0.123 tokens/byte";
if ($amount >= 42000)
bounce("amount too large");
if (balance[base] < 20000)
return;
操作符优先级
操作符按降序“粘性”的顺序具有以下优先级:
^
!
*
,/
,%
+
,-
,||
==
,!=
,>
,>=
,<
,<=
AND
OR
?:
OTHERWISE
限制
- 字符串不能超过4096个字符;
- state var value字符串不能超过1024个字符;
- state var names不能超过128个字符;
- 数字必须在IEEE754双倍范围内;
- 所有中间计算均四舍五入至15位有效数字;
- 所有脚本的总复杂度不能超过100;
- 所有脚本的操作总数不能超过2000。
任何超过这些限制的尝试都将导致脚本执行失败。
版权所有。发布者:Alan During,转载请注明出处:https://bbfans.org/2019/08/05/obyte-aa-guide2-oscript/