Challenges > Web3 前端训练营招募|2024,成为 Web3 开发 > Web3前端进阶实战
Aspect 引入了一种动态机制,以在区块链平台上实现系统级功能。Aspect 基本上类似于一个系统扩展,以 WebAssembly (WASM) 形式执行,可以绑定到任何智能合约上。这种特性提供了可以增强目标合约的功能或监控其活动,在满足预定义条件时触发特定操作的灵活性。
Aspect 的强大之处在于其基于 WebAssembly (WASM)。在 Artela 上,我们开发了一个专门的 WASM 运行时,名为 Aspect-Runtime,专用于在平台上执行 Aspects。Aspect Runtime 通过一组预定义的 host APIs 实现 Aspect 与区块链核心模块之间的交互。这些 APIs 使得 Aspect 具备多种能力,如读取区块链状态、调用智能合约、管理自身状态等。
有网站开发背景的人对于“中间件(middleware)”这个概念可能会比较熟悉。对于其他开发者,以下是更详细的说明。
拦截器是一段可以集成到 Web 服务器中以扩展其功能的代码。想象一下使用拦截器为网站服务器添加认证或日志记录功能,如下所示:
当接收到 HTTP 请求时,网站服务器的拦截器机制(在某些框架中称为中间件)允许开发人员创建模块来处理此传入请求,可以在主要处理之前或之后进行。例如,认证拦截器可以在请求到达核心逻辑之前验证用户凭据。这种设计模式使得认证和日志记录等功能模块化,确保设计的灵活性。
这种拦截器设置还支持共享上下文,使不同的中间件或路由器之间可以进行通信。例如,在认证用户之后,相关拦截器可以将用户数据存储在共享上下文中,后续的拦截器或路由处理器可以访问这些数据而无需重新加载。
相应地,Aspect 可以被视为智能合约的拦截器。想象一下在 Artela 上开发一个去中心化交易所并集成 Aspects:
正如拦截器可以增强网站框架,Aspect 也可以增强你的 dApps。它们支持模块化设计,并通过共享上下文促进模块间的通信。
在 Artela 中,我们提供了一个内置的扩展层,它位于应用层(包括智能合约)和基础层(区块链核心模块,如共识、内存池、P2P 网络)之间。这个扩展层允许开发人员为特定智能合约开发链级扩展,定制交易处理流程。
Aspect Core是一个系统模块,负责管理 Aspects 的生命周期,包括部署、执行、绑定、升级和销毁。它也作为一个系统合约部署在 0x0000000000000000000000000000000000A27E14
,可以通过 EVM 合约调用。Aspect 核心的实现是用本地代码编写的,以减少 VM 启动的开销。
Aspect 的生命周期由 Aspect Core管理,包括以下阶段:
bind
方法完成的。operation
接口触发。有关 Aspect 生命周期的更多详情,请参阅 此处 的文档。
即时调用(Just-in-Time Call)是一种可以在合约调用层连接点插入到当前 EVM 调用栈中的调用。有关即时调用概念的详细解释,请参阅 此处 的文档。
虽然 Aspect 和智能合约的执行环境是不同的(WASM 与 EVM),但它们并非完全隔离。可以通过 Aspect Context 在 Aspects 和智能合约之间共享信息。Aspect Context 本质上是一种临时存储,其生命周期仅限于当前交易。Aspect Context 可用于在 Aspect 和智能合约之间进行双向通信。
有关此主题的更多详情,请阅读 此处 的文档。
Aspect 可以对 EVM 智能合约进行只读调用,获取最新状态(目前不允许查询历史状态)。在某些情况下,比如构建一个价格预言机 Aspect 时,可以利用此功能从预言机合约中查询最新价格,并将其保存到 Aspect Context,与智能合约共享。有关此主题的更多详情,请阅读 此处 的文档。
Aspects 可以追踪智能合约状态的变化。例如,Aspect 可以基于输入参数检查智能合约的提交状态是否如预期。
这种追踪由 ASOLC 编译器生成的额外操作码和中间表示方法来实现:
有关此主题的更多信息,请参阅 ASOLC 文档。
类似于智能合约,Aspect 也是有状态的。每个 Aspect 维护其自身状态,这些状态将被持久化在全局状态中。目前 Aspect 仅提供一个简单的键值存储,更好的存储绑定将在后续版本中提供。要将数据持久化到区块链全局状态中,可以使用以下方法:
// read & write state
let state = sys.aspect.mutableState.get<string>('state-key');
let unwrapped = state.unwrap();
state.set('state-value');
Aspect 也可以独立操作。它可以通过 bytes-in-bytes-out 的 operation
方法处理外部交易或调用:
class AspectTest implements IAspectOperation {
operation(input: OperationInput): Uint8Array {
// handle incoming request and echo it back
return data;
}
}
注意
请注意,operation 方法只是一个 bytes-in-bytes-out 的入口点,如果 Aspect 中有任何敏感数据,请确保正确进行身份验证。
Artela Aspect 的初始版本是使用 AssemblyScript(TypeScript 的一个子集,具有严格的类型)构建的。为了方便 Aspect 的开发,我们还创建了 Aspect Tooling,这是一个用于与底层 host APIs 交互的库和工具集。有关更多详情,请查看我们的仓库。
每个Join Point表示特定交易生命周期阶段的状态转换函数。这些Join Point由不同的元数据(概述触发条件、可以访问的系统模块以及可以利用的运行时上下文)和一个入口函数(例如,你的 Aspect 类中的 PreTxExecute
方法)构成。
连接点分为以下几类:
更直观的表示请参见下图:
每个Join Point都有一个专用的入口函数接口,包括:
要使 Aspect 在Join Point内处于活动状态,它必须包含与给定入口函数对齐的函数,通过相应的入口函数启动 Aspect。
互操作接口在以下方面发挥作用:
通过这些接口,Aspects 可以利用核心区块链功能。一些连接点可能提供多个系统调用。这些接口使 Aspects 能够定制交易或区块处理流程。
在 Aspect 编程中,在区块和交易的生命周期中定义了各种 join points 。此框架使 Aspect 开发者能够为 dApps 创建自定义增强功能。
每个 join point 的全面概述:
Join Point | 描述 |
---|---|
FilterTx | 当 RPC 服务器接收到此交易时触发,请注意此 join point 在共识之外,因此不允许在此处修改 Aspect 状态。 |
OnTxVerify | 当交易签名正在验证时触发,Aspect 可以用自定义验证逻辑替换内置的 secp256k1 签名验证。 |
OnBlockInitialize | 在准备区块提案之前激活,允许在此时插入自动化交易。 |
PreTxExecute | 在交易执行之前触发。在此阶段,账户状态保持原始状态,允许 Aspect 根据需要预加载信息。 |
PreContractCall | 在跨合约调用执行之前触发。例如,在 TX 执行期间,Uniswap 合约调用 Maker 合约,Aspect 将被执行。 |
PostContractCall | 在跨合约调用执行之后触发。Aspect 可以检查合约的 post-call 状态,并做出后续执行决策。 |
PostTxExecute | 一旦交易执行完毕且账户状态已定型后激活。随后,Aspect 可以对最终执行状态进行全面审查。 |
OnTxCommitted | 在交易定型后触发,交易引起的修改状态已刷新到状态数据库。在此阶段,Aspect 可以进行后处理活动,例如启动可以在未来区块中执行的异步任务。 |
OnBlockFinalize | 在区块定型后触发。允许 Aspect 提交可以在未来区块中执行的异步任务。 |
值得注意的是,join points 提供了抽象定义,适应多种区块链实现、不同的 join point 暴露和其他平台特定属性。
运行时上下文为 Aspects 提供了关于交易和区块处理的关键见解,包括智能合约状态更新、记录的事件和交易原始数据。
上下文被设计为键值对集合,当 Aspect 使用特定键查询时,上下文提供最新的关联值。例如,使用 tx^content^nonce
查询将返回初始交易的 nonce 字段值。为了保持共识机制的对齐和一致性,区块链网络中的所有节点必须为区块中的特定交易键提供一致的值。
上下文包括四个主要类别:
上下文的生命周期仅限于区块处理期间。当一个区块开始处理时,区块中的每个交易都获得一个新的上下文,包含交易特定的详细信息,确保交易执行期间的一致性。
在区块处理结束时,上下文终止。需要理解的是,这种终止不会影响区块链状态或交易结果,两者都永久存储在区块链上。上下文主要保存临时数据,帮助交易过程并维持区块内的状态一致性。
Aspect 的生命周期包括多个阶段:部署、升级、配置、绑定、解绑、执行和销毁。
需要注意的是,Aspect 核心是一个位于地址 0x0000000000000000000000000000000000A27E14
的系统合约,管理所有 Aspect 生命周期操作。有关 Aspect 核心 ABI 的更多详细信息,请参考 此链接。
部署一个 Aspect 类似于部署传统智能合约。通过 EOA 交易部署 Aspect 时,需要提供以下关键信息:
参数名称 | 必要性 | 描述 |
---|---|---|
code | 是 | Aspect 的 WASM 工件字节码,以十六进制格式表示。 |
properties | 否 | Aspect 的初始只读状态。 |
account | 是 | 结算账户,负责支付 Aspect 的 gas 费用。某些 Aspect 操作会产生 gas 成本。目前,结算账户默认是合约调用的发送者,但未来版本将支持自定义结算账户。 |
proof | 否 | 未来支持自定义结算账户绑定验证的占位符。 |
与智能合约一样,一旦部署,Aspect 将接收一个唯一的 ID,等同于 EVM 地址类型(20 字节)。初始部署时,Aspect 的版本为 1.
Aspect 的状态可以表示为 JSON 形式:
{
"id": "0xABCDEF....",
"code": {
"1": "0xABCDEF...."
},
"properties": {
"property-name": "property-value",
...
},
"settlementAccount": "0xABCDEF....",
"currentVersion": 1
}
升级 Aspect 不会中断其当前的绑定状态。绑定到旧版 Aspect 的合约将继续执行旧代码。请确保你的 Aspect 具有向后兼容性,以防发生意外。
Binding将 Aspect 与特定智能合约关联起来。只有通过合约中定义的 isOwner(address): bool 方法验证的智能合约所有者才能发起此过程。
绑定过程需要:
参数名称 | 必须 | 描述 |
---|---|---|
aspectId | 是 | 要绑定的 Aspect 的 ID。 |
aspectVersion | 是 | 要绑定的 Aspect 的版本。使用 0 绑定到最新版本。 |
account | 是 | 要与 Aspect 绑定的账户地址。 |
priority | 是 | Aspect 的执行优先级。数字越小,优先级越高。对于优先级相同的 Aspect,先绑定的先执行。 |
Aspect 核心合约记录绑定关系如下:
{
"0x{AccountAddress}": [
{
"aspectId": "0x{AspectId1}",
"aspectVersion": 1
},
{
"aspectId": "0x{AspectId2}",
"aspectVersion": 2
}
...
]
}
绑定是基于 Aspect 开发中的一个重要过程。只有当 Aspect 绑定到特定智能合约时,Aspect 才能在某些 join points 被触发。EoA 也可以与 Aspect 绑定,Aspect 可以为 EoA 提供自定义交易验证过程。
isOwner(address) returns (bool)
方法以验证发送者的地址。如果验证失败(例如,由于未实现的验证方法或地址验证失败),绑定交易将被回滚。onContractBinding(address) bool
方法。这检查当前智能合约是否可以与给定 Aspect 绑定。如果此验证失败,交易将被回滚。注意:只有在给定版本的 Aspect 和智能合约都已部署时,才能建立绑定关系。不能与不存在的 Aspect 或智能合约绑定。
为了支持与 Aspect 绑定,智能合约必须实现 isOwner(address) returns (bool)
方法。此方法对于验证智能合约的所有权至关重要。如果方法调用失败或返回 false,绑定交易将不会成功。在此方法中实现自定义逻辑可以进行更复杂的所有权验证,例如多签名验证。
Aspects 可以拒绝来自某些合约的绑定。当智能合约尝试与 Aspect 绑定时,调用 onContractBinding(address) bool
钩子。如果此方法返回 false 或未实现,绑定交易将失败。要将 Aspect 限制为某些合约,可以在此方法中实现白名单检查。或者,为了公共可访问性,简单返回 true。
绑定请求中的优先级是一个无符号的 8 位整数。它决定了 Aspect 执行的顺序,优先级编号最低的 Aspect 最先执行。如果多个 Aspects 具有相同的优先级,则最早绑定的 Aspect 最先执行。
一个账户最多可以绑定 255 个 Aspects,但这取决于 Aspect 类型。特殊情况是交易验证器 Aspect,如果你为合约账户绑定这种 Aspect,每个账户只能绑定一个 Aspect。
Aspect 可以从从智能合约中分离。只有智能合约的所有者才能发起解绑,其地址必须通过 isOwner(address): bool
验证。
要解绑,则需要:
参数名称 | 必须 | 描述 |
---|---|---|
aspectId | 是 | 要解绑的 Aspect 的 ID。 |
account | 是 | 要分离的账户地址。 |
一旦解绑,当接收到与给定账户相关的交易时,Aspect 将不会执行。
Aspect 执行在以下两种情况下触发:
在 Aspect 中,有一组预定义的方法会在特定连接点处触发。在交易处理的某个阶段,Aspect 的入口函数会被调用,并将路由到与当前连接点配对的相应方法。例如,PreContractCall
方法将在合约调用之前执行。
每个 Aspect 都有一个 bytes-in-bytes-out
的 operation
方法。此方法是维护接口,允许 Aspect 维护者通过交易 / 调用更新或获取 Aspect 状态。如果你的 Aspect 包含敏感数据,请确保在修改状态之前实现必要的授权检查。目前,operation
方法以 bytes in bytes out
格式工作,开发人员需要自行管理编码、解码和路由。未来版本将提供更简化的解决方案。
Aspect 编程引入了一种围绕连接点模型 (JPM) 构建的强大范式。该模型围绕三个关键组件展开:
为说明 Aspect 编程的工作原理,考虑一个包含大量存款的保险库智能合约。目标是在运行时保护智能合约,使其免受可能非法重定向存款的潜在威胁。一个 Aspect 示例被设计用于监督和验证智能合约执行后的保险库变更。如果检测到资金流动异常,此 Aspect 将取消可疑事务。此 Aspect 的执行在智能合约执行后、事务调用合约时启动。
一个 Aspect 被设计为clas,是基础 Aspect 接口的扩展。它包含指示Join Points的方法,添加的逻辑可以在这些方法中交织。以下是一个示例:
class Aspect implements IPostTxExecuteJP {
/**
* postTxExecute is a join-point invoked post the completion of transaction execution but prior to state commitment.
*
* @param ctx context of the designated join-point
* @return outcome of the Aspect's execution
*/
postTxExecute(input: PostTxExecuteInput): void {
if (ethereum.parseMethodSig(ctx.tx.content.data) == ethereum.computeMethodSig('withdraw')) {
const withdrawAmount = ...// extract withdraw amount from calldata via context.tx.content.data
const vaultBalance = new VauleState(ctx, ctx.tx.content.to).get('vauleBalance').current();// state change wrapper class generated by aspect-tool
if (vaultBalance != withdrawAmount) {
sys.revert('Error: Balance discrepancy detected.');
}
}
}
}
在上述示例中,postTxExecute
方法充当join point的入口,在 EVM 完成事务后发挥作用。
PostTxExecuteCtx
对象表示运行时上下文,提供了对原始事务及其在当前join point上的细节的洞察。它被传递给join point方法,促进其与正在进行的事务的交互。
示例 Aspect 在事务期间检查智能合约的 withdraw
函数的调用。如果被调用,事务参数中的预期提款金额将与保险库的实时资金流动进行交叉检查。发现差异(通常是编码错误或恶意攻击)时,Aspect 将激活 revert()
函数,通过运行时上下文的事务管理对象来撤销该事务。
要将一个 Aspect 集成到区块链中,其字节码需要嵌入到一个部署事务中。这个事务与一个 Aspect 系统合约交互,将 Aspect 的字节码记录到区块链的全局状态中。这允许验证者访问字节码,使其在激活时能够执行 Aspect 的逻辑。
然而,一个 Aspect 只有在绑定到特定智能合约时才会启动。它需要智能合约所有者使用其外部拥有的账户 (EOA) 签署一个绑定事务 - 该账户应绕过智能合约的 isOwner(address) returns (bool)
方法的检查。这个事务包含智能合约地址和 Aspect ID,并在执行时涉及 Aspect 系统合约,将智能合约和 Aspect 在区块链的全局状态中绑定起来。这确保只有智能合约的合法所有者可以绑定 Aspect,防止任何未授权的绑定尝试。
在成功执行部署和绑定事务后,Aspect 被集成到区块链并绑定到智能合约中,增强了合约的功能并提高了其安全性。
每当智能合约被事务调用时,Aspect 编程框架运行时将被激活。随着事务的开始,Aspect 运行时评估每个join point,识别与调用的智能合约相关联的 Aspect。如果发现匹配,Aspect 在事务执行后被触发。
要执行 Aspect,它的字节码从区块链的全局状态中获取,并引入到 WebAssembly (WASM) 运行环境中。随后,创建一个详细描述事务及其环境的上下文对象。这为 Aspect 运行时调用 Aspect 的主要功能设置了舞台,促使其逻辑执行。
例如,考虑一个智能合约中的 withdraw
函数,设计用于确保资金转账始终超过函数参数中规定的金额。如果一个 Aspect 绑定到 postTxExecute
的join point,一旦事务结束,它将被激活。Aspect 运行时检索字节码,在 Wasm 环境中启动它,创建一个上下文对象,并调用 Aspect 的主要功能。如果 Aspect 检测到资金转账中的任何差异,它可以通过join point上下文标记一个反转。Aspect 运行时在识别到此标志时,将撤销事务,将其标记为失败,并提供交易因关联 Aspect 被推翻的理由。
Aspect 编程框架的核心是Join Point模型 (JPM)。该模型在事务和区块处理序列中提供战略插入点,为附加逻辑铺平了道路。Aspect 体现了这些附加逻辑,可以进入运行时上下文并进行系统调用,将其加入到交易的生命周期中。通过将 Aspect 绑定到指定的Join Points,智能合约所有者可以增强合约的功能并提高其安全性。
后续章节将深入探讨Join Point模型的复杂设计及其实现。
本节将指导您在 Artela 上使用示例 Aspect 构建 dApp。此 Aspect 作为原生扩展,与智能合约协同处理,并且可以在交易生命周期的各个阶段注入。在本例中,我们将展示 Aspect 如何识别并回滚特定交易。
确保您已安装最新版本的 Node.js 和 npm,首先安装 aspect-tool:
npm install -g @artela/aspect-tool
项目初始化:要使用 aspect-tool 启动项目,请按照以下步骤操作:
# 创建一个新目录并进入该目录
mkdir my-first-aspect && cd my-first-aspect
# 使用 aspect-tool 初始化 npm 项目
aspect-tool init
# 安装必要的依赖项
npm install
这将创建一个具有以下结构的项目目录:
.
├── README.md
├── asconfig.json
├── aspect <-- Aspect代码放在这里
│ └── index.ts <-- Aspect的入口函数
├── contracts <-- 智能合约放在这里
├── package.json
├── project.config.json
├── scripts <-- 实用脚本,包括部署、绑定等
│ ├── aspect-deploy.cjs
│ ├── bind.cjs
│ ├── contract-call.cjs
│ ├── contract-deploy.cjs
│ ├── contract-send.cjs
│ └── create-account.cjs
├── tests
└── tsconfig.json
在项目主目录的 contracts 项中,创建扩展名为 .sol 的智能合约源文件。
例如,创建一个 HelloWorld.sol 文件:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
contract HelloWorld {
address private owner;
constructor() {
owner = msg.sender;
}
function isOwner(address user) external view returns (bool result) {
return user == owner;
}
// print hello message
function hello() public pure returns (string memory) {
return "hello";
}
// print world message
function world() public pure returns (string memory) {
return "world";
}
}
此步骤依赖于 solc,首先检查 solc 是否正确安装:
solc --version
使用以下命令编译您的合约:
npm run contract:build
✅ 成功编译后将在 build/contract 目录中生成 HelloWorld.abi 和 HelloWorld.bin 文件。
在根目录中的 project.config.json 中更新适当的网络配置:
{
"node": "https://betanet-rpc1.artela.network"
}
💡 有关开发环境设置的更多详细信息,请参阅 artela devnet。
在 my-first-aspect 文件夹下执行以下命令以创建账户(如果尚未创建):
npm run account:create
✅ 如果账户创建成功,其私钥将作为 privateKey.txt 转储在当前目录中。
💡 有关此命令的详细用法,请参阅 create-account 命令文档。
如果您的账户缺少测试代币,请加入 Discord,并在 testnet-faucet 频道领取一些。
在 my-first-aspect 文件夹中执行以下命令,使用提供的脚本部署合约:
npm run contract:deploy -- --abi ./build/contract/HelloWorld.abi \
--bytecode ./build/contract/HelloWorld.bin
✅ 部署成功后,终端将显示合约地址。
💡 有关此命令的详细用法,请参阅 deploy-contract 命令文档。
在 my-first-aspect 文件夹中执行以下命令,调用合约:
npm run contract:call -- --contract {smart-contract-address} \
--abi ./build/contract/HelloWorld.abi \
--method hello
将占位符 {smart-contract-address} 替换为步骤 2.3 中部署智能合约时获得的信息。
✅ 成功后,终端将显示调用结果。
💡 有关此命令的详细用法,请参阅 contract-call 命令文档。
npm run contract:call -- --contract {smart-contract-address} \
--abi ./build/contract/HelloWorld.abi \
--method world
✅ 如果返回 world 字符串,则说明 HelloWorld 合约已经成功部署。
Aspect 源文件可以在 aspect/index.ts 中找到。
例如,要在智能合约调用执行后添加逻辑,打开 index.ts,找到 postContractCall 函数,并插入您的逻辑:
typescript复制代码
postContractCall(input: PostContractCallInput): void {
// Implement me...
}
💡 有关详细说明,请参阅 Aspect 文档。
要将 HelloWorld 合约的状态与您的 Aspect 集成,请按照以下步骤操作:
在 aspect/index.ts 中,添加您的 Aspect 以检查交易,如果调用了 world 函数,则回滚:
import {
allocate,
entryPoint,
execute,
IPostContractCallJP,
PostContractCallInput,
sys,
uint8ArrayToHex,
} from "@artela/aspect-libs";
// 1. implement IPostContractCallJP
class Aspect implements IPostContractCallJP {
isOwner(sender: Uint8Array): bool {
// implement me
// if return false,bind、unbind、upgrade Aspect will be block
return true;
}
/**
* postContractCall is a join-point which will be invoked after a contract call has finished.
*
* @param input input to the current join point
*/
postContractCall(input: PostContractCallInput): void {
let txData = uint8ArrayToHex(input.call!.data);
// if call `world` function then revert, 30b67baa is method signature of `world`
if (txData.startsWith("30b67baa")) {
sys.revert("the function `world` not available");
}
}
}
// 2.register aspect Instance
const aspect = new Aspect();
entryPoint.setAspect(aspect);
// 3.must export it
export {execute, allocate};
构建您的 Aspect:
npm run aspect:build
✅ 生成的 release.wasm 文件将在 build 文件夹中包含必要的 WASM 字节码。
部署编译后的 Aspect:
npm run aspect:deploy -- --wasm ./build/release.wasm --joinPoints PostContractCall
✅ 成功执行后,终端将显示 Aspect 地址。请务必记录此地址,因为稍后会用到。
💡 有关此命令的详细用法,请参阅 deploy-aspect 命令文档。
部署 Aspect 并不会自动激活它。要使其生效,需要将其绑定到智能合约:
npm run contract:bind -- --contract {smart-contract-address} \
--abi ./build/contract/HelloWorld.abi \
--aspectId {aspect-Id}
将占位符 {smart-contract-address} 替换为步骤 2.3 中部署智能合约时获得的信息。将占位符 {aspect-Id} 替换为步骤 3.4 中部署 Aspect 时获得的信息。
✅ 绑定成功,会输出交易收据。
💡 有关此命令的详细用法,请参阅 bind-aspect 命令文档。
现在 HelloWorld 合约和 Aspect 已绑定,调用 world 方法进行测试:
npm run contract:call -- --contract {smart-contract-address} \
--abi ./build/contract/HelloWorld.abi \
--method world
将占位符 {smart-contract-address} 替换为步骤 2.3 中部署智能合约时获得的信息。
✅ 由于 Aspect 拦截,交易已回滚。
恭喜!您已经学习了 Aspect 开发的基础知识。要深入了解,请参阅我们全面的 Aspect 文档。
© 2025 OpenBuild, All rights reserved.