新消息:Helius 收购 Light Protocol
灯泡和电路板
博客/基础知识

如何将你的想法变成 Solana 程序(智能合约)

开发者教育X 上的 Mike MacCanaLinkedIn 上的 Mike MacCana
阅读需 7 分钟

让我们来设计一个 Solana 程序。Solana 生态中有大量文章介绍如何理解 Solana 编程模型,还有一些小型演示程序,非常适合初学者学习 Solana 和 Anchor。不过,实际程序需要考虑的问题更加复杂。 

本文将分析一个我们想要构建的实际程序,并介绍: 

  1. 将数据定义为 Solana 账户
  2. 在数据之间建立关系
  3. 在程序中存储代币
  4. 使用索引加快读取速度
  5. 使用分片加快写入速度

本示例旨在帮助你将自己的想法建模为 Solana 程序和账户。 

注意:本文假设你已熟悉 Anchor 基础知识。 

Solana 程序示例——构建预测市场

我们今天要设计的是一个类似 Polymarket、Hedgehog 或 Drift BET 的预测市场。如果你不了解预测市场,它允许人们对某个事件的不同结果下注。例如,哪支球队会赢得超级碗、谁会获得奥斯卡“最佳导演奖”、政府是否会在特定时间前发布公告,或者任何其他具有明确结果的现实事件。如果人们押中了获胜结果,就能从投注池中获得奖金。

预测市场架构

以下是预测市场的核心架构,以及各项内容之间的关系:

  • 存在多个事件。
  • 每个事件都有多个结果。最终,其中一个结果会被确定为获胜结果。
  • 用户会对每个结果进行多次投注。用户下注时,资金会加入该事件的奖金池。

事件“结算”后(即确定获胜结果后):

  • 对获胜结果下注的用户可以领取奖金
  • 获胜者将获得奖金池的一部分资金(扣除平台抽成)
  • 每位获胜者在奖金池中的份额,取决于其投注额占获胜结果总投注额的比例。

1. 将应用设计为 Solana 账户

如果使用关系型数据库设计这个程序,我们会考虑:

  • 将相似的数据项存储为表中的行 
  • 使用主键唯一标识每条数据
  • 通过表中的列定义每个数据项应有的属性及其类型

在 Solana 中,这些概念大致对应:

  • 使用相同的账户类型存储相似的数据项 
  • 使用地址唯一标识每条数据
  • 使用 struct(键和数据类型)定义每个数据项的属性

下面用传统数据库表和 Solana 账户展示同一组数据:

2. 映射数据之间的关系

在 Solana 中,数据项之间关系的处理方式截然不同。传统数据库使用关系(即表之间的逻辑连接)。Solana 将一对多关系处理为地址向量,其中每个地址都对应一个包含该数据项的账户。 

例如,事件包含“结果”,这些结果是由结果地址组成的向量。Anchor 将其表示为 Vec<Pubkey>,不过PDA 地址(例如我们的结果地址)并不是真正的公钥。

3. 存储代币

与传统数据库不同,Solana 程序还可以在账户中存储实际资金,而不只是余额数字。

在这个示例中,事件需要一个代币账户来存放奖金池。事件的 PDA 将拥有该奖金池账户。用户下注时会将代币发送到这个账户;更重要的是,当用户领取奖金时,我们的程序会以事件账户的身份签署交易,将代币从奖金池中转出。

4. 降低读取开销

我们的程序需要尽可能快速地响应用户。作为开发者,我们也希望避免为不必要的账户读取付费。通过维护累计值和建立索引,可以让程序响应更快、效率更高。

计算累计值

用户领取奖金时,我们需要准确知道每个结果的投注总额。预测市场根据公式 奖金池 × 投注金额 ÷ 获胜结果的投注总额 计算获胜用户的奖金。

目前,我们只在每个投注账户中存储该笔投注的金额。要计算某个结果的投注总额,就必须读取该结果下的所有投注账户,再将金额相加。

我们可以改为给每个结果添加一个字段——total_amount——并在用户下注时递增该字段。这样,用户获胜后,我们无需读取该结果下的每一笔投注,就能轻松确定奖金金额。

使用索引

我们还需要查找来自某个用户账户的所有投注。可以使用 getProgramAccounts() 获取所有投注账户,再筛选出投注者地址设置为该用户地址的账户。虽然 Helius 的高速 getprogramAccounts() 比其他 RPC 提供商快得多,但索引也是一种常见的替代方案。 

因此,我们来创建一个索引,存储每位用户的投注。用户进行新投注时,如果该数据项尚不存在,我们就创建它,并将该笔投注添加到用户的投注列表中:

我们还会为每个事件添加标签,以便轻松查找带有 'sports'、‘politics’、'europe'、'usa'、'politics' 等标签的所有事件。为此,我们再创建一个索引:

现在,只需查看事件标签账户,就能轻松检索所有相关事件。

5. 减少写入争用

还记得每个事件都有一个用于存储该事件投注资金的代币账户吗?每当用户新增投注时,代币都会转入这个账户。换句话说,这个账户会发生写入。

Solana 之所以速度快,是因为它会并行执行操作。但是,更新单个账户的余额无法并行完成,只能按顺序执行,因为任意时刻每个账户都必须只有一个确定的余额。 

如果一个新事件公布后涌入大量投注,事件的代币账户就会同时发生大量写入,我们程序的交易可能会显得很慢。这就是写入争用——多笔交易同时争夺账户的访问权。

在这种情况下,实现并行处理的一种方式是使用分片。将一项资源拆分成多个可并行访问的部分,这些部分称为分片。

分片的工作原理

根据投注者公钥最后一个字节的值,使用 shard_num() 宏将传入付款发送到不同的 win_pool 分片。这样可以确保快速接收付款。

之后,管理员指令处理程序可以将这些资金合并到单个奖金池账户中,确保同一账户内有足够的流动性向获胜者支付奖金。

如果热门事件完成结算,所有获胜者同时领取奖金,我们也需要考虑写入争用。我们还需要尽快从奖金池中转出资金。

最好的选择是避免设置领取流程。我们可以在事件完成结算后立即按顺序发送资金,这样用户完全不必面对缓慢的领取过程,因为奖金已经存入了他们的账户。

不过,只有真正拥有用户后,你才需要担心大批用户同时下注或领取奖金。如果你正在规划 Solana 程序但尚未发布,就没必要为了并不存在的大量用户流量进行优化。如果发布时不需要这种大型优化,你应该权衡它所带来的额外复杂性,同时也要意识到,随着程序越来越受欢迎,将来可能仍需实现这些优化。

总结

本文深入探讨了如何在 Solana 上创建一个现实应用。现在,你已经掌握了定义 Solana 账户、建立数据关系、存储代币,以及使用索引和分片优化性能的实用知识。 

如果你还有任何问题,欢迎联系 X 上的 @helius 或加入 Helius Discord。未来我们还会继续扩展这个预测市场示例,请务必关注这些账户以获取最新动态。

掌握这些新技能后,是时候将你的想法变成 Solana 程序,并在 Solana 生态中将其变为现实了。祝你编码愉快! 感谢 Ichigo 审阅本文,也感谢 r0bre 指出这里使用的账户分片技术。

更多资源

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片