在 sCrypt 合约中使用 HashedMap 数据结构

在 sCrypt 合约中使用 HashedMap 数据结构

在日常程序开发中,Map 作为一种常见的数据结构被经常使用,其支持增删改查等操作,并且通常具有 O(1) 或 O(logN) 的查询效率。为了方便开发者使用,我们在 sCrypt 语言中也实现了一个具有类似功能的标准库 HashedMap

HashMap

在 sCrypt 合约中完整实现 Map 的问题

起初我们计划完整地实现一个类似其他主流语言中的 Map 类,但很快就发现了这样做有几个问题和难点:

  1. Map 的底层实现通常来说都比较复杂,比如常见的 HashMap 底层实现会涉及哈希函数的选取、数据桶内存管理等逻辑,如果全部实现脚本的体积会非常大;

  2. 因为 sCrypt 脚本中循环次数必须为常量,这给实现又增加了一层困难;

即使这样,并不意味着我们无法实现一个类似的数据结构,关键在于思路转换。

设计巧妙的 HashedMap

实际上,我们在合约内使用 Map 只需要满足可验证的特性即可,这与合约设计时的核心思路一致。从这个点出发,可以设计一个更轻量级的 Map,我们称之为 HashedMap。它具有以下特点:

  • 不存储原始的键值(Key/Value)数据,仅存储它们的哈希值;
  • 内部按照键(Key)的哈希值严格增序存储;
  • 访问时需要提供相应 Key 的排序索引值;

这样的好处是能简化合约中代码逻辑,同时在满足性能的前提下可以进行增删改查操作。此外,为了区别于常见的 HashMap 数据结构,命名上也采用了 HashedMap 以示不同。

使用泛型

因为 HashedMap 中键和值的数据类型可以是 sCrypt 中的任意数据类型,需要根据需求灵活声明,所以我们增加了泛型合约的支持,使得 HashedMap 成为了一个泛型库,其声明如下:

library HashedMap<K, V> {
	constructor(bytes data){...}
	...
}

这里的 KV 分别对应着 map 中键(key)和值(value)的类型。 在合约中使用时,可使用如下语法进行声明:

  1. 完整定义:
HashedMap<int, int> map = new HashedMap<int, int>(b'');
  1. 右边简写定义:
HashedMap<int, int> map = new HashedMap(b'');
  1. 左侧 auto 定义:
auto map = new HashedMap<int, int>(b'');

使用示例

这里我们给一个将 HashedMap 作为状态数据进行存储的合约示例,其代码如下:

struct MapEntry {
    int key;
    int val;
    int keyIndex;
}

contract StateMap {

    @state
    bytes _mpData; // storage of the serialized data of the map

    // Add key-value pairs to the map
    public function insert(MapEntry entry, SigHashPreimage preimage) {
        require(Tx.checkPreimage(preimage));
        HashedMap<int, int> map = new HashedMap(this._mpData);
        int size = map.size();
        require(!map.has({entry.key, entry.keyIndex}));
        require(map.set({entry.key, entry.keyIndex}, entry.val));
        require(map.canGet({entry.key, entry.keyIndex}, entry.val));
        require(map.size() == size + 1);
        require(this.passMap(map.data(), preimage));
    }

    // update key-value pairs in the map
    public function update(MapEntry entry, SigHashPreimage preimage) {
        require(Tx.checkPreimage(preimage));
        HashedMap<int, int> map = new HashedMap(this._mpData);
        require(map.has({entry.key, entry.keyIndex}));
        require(map.set({entry.key, entry.keyIndex}, entry.val));
        require(map.canGet({entry.key, entry.keyIndex}, entry.val));
        require(this.passMap(map.data(), preimage));
    }

    // delete key-value pairs in the map
    public function delete(int key, int keyIndex, SigHashPreimage preimage) {
        require(Tx.checkPreimage(preimage));
        HashedMap<int, int> map = new HashedMap(this._mpData);
        require(map.has({key, keyIndex}));
        require(map.delete({key, keyIndex}));
        require(!map.has({key, keyIndex}));
        require(this.passMap(map.data(), preimage));
    }

    // update state _mpData, and build a output contains new state
    function passMap(bytes newData, SigHashPreimage preimage) : bool {
        this._mpData = newData;
        bytes outputScript = this.getStateScript();
        bytes output = Utils.buildOutput(outputScript, SigHash.value(preimage));
        return (hash256(output) == SigHash.hashOutputs(preimage));
    }
}

在上面的这个合约示例中,我们展示了 HashedMap 的几个可用函数,具体包括:

添加元素

添加键值对(key-value)可以使用 set(SortedItem<K> key, V val) 方法。如:

bool r = map.set({entry.key, entry.keyIndex}, entry.val);

与其它语言的 map 不同的是添加键值需要传递 keyIndex 参数。这里我们用 SortedItem<K> 类型绑定了 keykeyIndex,作为参数它们必须同时提供。keyIndex 可以通过 scryptlib 提供的 findKeyIndex 函数在链下计算。例如在使用 typescript 编写的测试代码中:

let map = new Map<number, number>();
map.set(key, val);  //先把键值对添加到链外的 map

const tx = buildTx(map);
const preimage = getPreimage(tx, mapTest.lockingScript, inputSatoshis)
const result = mapTest.insert(new MapEntry({
    key: key,
    val: val,
    keyIndex: findKeyIndex(map, key) // 获取 `keyIndex` 参数
}), preimage).verify()

expect(result.success, result.error).to.be.true;

更新元素

更新元素和添加元素一样,都使用 HashedMap 合约的 set(SortedItem<K> key, V val) 方法。

查询元素

查询元素可使用 canGet(SortedItem<K> key, V val) 方法,如:

require(map.canGet({key, keyIndex}, val));

这里与其他语言 map 不同,并不能通过 key 获取对应的 val 值,而是将 keyval 以及 keyIndex 传入进行校验。当且仅当参数能够匹配到 HashedMap 中的特定元素时,才会返回 true 值。如果为真,合约里即可确定 val 就是 key 所对应的具体数据,可以进一步用于其他处理了。

类似的,另一个方法 has(SortedItem<K> key) 可用于检查是否包含某个特定的键,例如:

require(map.has({key, keyIndex}));

删除元素

可使用 delete(SortedItem<K> key) 方法来删除元素,如果删除的元素不存在会返回失败。同样需要在链外使用 findKeyIndex(map, key) 函数来计算 keyIndex。

//从链外map删除之前,先计算出keyIndex,并保存
const keyIndex = findKeyIndex(map, key);  
map.delete(key);

const tx = buildTx(map);
const preimage = getPreimage(tx, mapTest.lockingScript, inputSatoshis)

// 调用合约的删除方法需要提供key, keyIndex
const result = mapTest.delete(key, keyIndex, preimage).verify()  

expect(result.success, result.error).to.be.true;

总结

以上就是针对在 sCrypt 合约中使用 HashedMap 的一点说明,希望对大家的开发能够有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sCrypt Web3应用开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值