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

Posted freedomhero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在 sCrypt 合约中使用 HashedMap 数据结构相关的知识,希望对你有一定的参考价值。

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

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

在 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 作为状态数据进行存储的合约示例,其代码如下:

import "util.scrypt";

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.val, entry.keyIndex));
        require(map.canGet(entry.key, entry.val, entry.keyIndex));
        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.val, entry.keyIndex));
        require(map.canGet(entry.key, entry.val, entry.keyIndex));
        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 = Util.buildOutput(outputScript, Util.value(preimage));
        return (hash256(output) == Util.hashOutputs(preimage));
    

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

添加元素

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

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

与其它语言的 map 不同的是添加键值需要传递 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(K key, V val, int keyIndex) 方法。

查询元素

查询元素可使用 canGet(key, val, keyIndex) 方法,如:

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

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

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

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

删除元素

可使用 delete(K key, int keyIndex) 方法来删除元素,如果删除的元素不存在会返回失败。同样需要在链外使用 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 的一点说明,希望对大家的开发能够有所帮助。

以上是关于在 sCrypt 合约中使用 HashedMap 数据结构的主要内容,如果未能解决你的问题,请参考以下文章

编写和发布 scrypt-ts 库合约

sCrypt 合约中的内联脚本

sCrypt 合约中如何使用优化版 OP_PUSH_TX

如何在 sCrypt 合约中实现浮点数运算

使用 sCrypt 实现一个简单的 NFT 合约

使用 sCrypt 实现一个简单的 NFT 合约