在 sCrypt 合约中使用 HashedMap 数据结构
Posted freedomhero
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在 sCrypt 合约中使用 HashedMap 数据结构相关的知识,希望对你有一定的参考价值。
在 sCrypt 合约中使用 HashedMap 数据结构
在日常程序开发中,Map 作为一种常见的数据结构被经常使用,其支持增删改查等操作,并且通常具有 O(1) 或 O(logN) 的查询效率。为了方便开发者使用,我们在 sCrypt 语言中也实现了一个具有类似功能的标准库 HashedMap
。
在 sCrypt 合约中完整实现 Map 的问题
起初我们计划完整地实现一个类似其他主流语言中的 Map 类,但很快就发现了这样做有几个问题和难点:
-
Map 的底层实现通常来说都比较复杂,比如常见的 HashMap 底层实现会涉及哈希函数的选取、数据桶内存管理等逻辑,如果全部实现脚本的体积会非常大;
-
因为 sCrypt 脚本中循环次数必须为常量,这给实现又增加了一层困难;
即使这样,并不意味着我们无法实现一个类似的数据结构,关键在于思路转换。
设计巧妙的 HashedMap
实际上,我们在合约内使用 Map 只需要满足可验证的特性即可,这与合约设计时的核心思路一致。从这个点出发,可以设计一个更轻量级的 Map,我们称之为 HashedMap。它具有以下特点:
- 不存储原始的键值(Key/Value)数据,仅存储它们的哈希值;
- 内部按照键(Key)的哈希值严格增序存储;
- 访问时需要提供相应 Key 的排序索引值;
这样的好处是能简化合约中代码逻辑,同时在满足性能的前提下可以进行增删改查操作。此外,为了区别于常见的 HashMap 数据结构,命名上也采用了 HashedMap
以示不同。
使用泛型
因为 HashedMap 中键和值的数据类型可以是 sCrypt 中的任意数据类型,需要根据需求灵活声明,所以我们增加了泛型合约的支持,使得 HashedMap
成为了一个泛型库,其声明如下:
library HashedMap<K, V>
constructor(bytes data)...
...
这里的 K
和 V
分别对应着 map 中键(key)和值(value)的类型。 在合约中使用时,可使用如下语法进行声明:
- 完整定义:
HashedMap<int, int> map = new HashedMap<int, int>(b'');
- 右边简写定义:
HashedMap<int, int> map = new HashedMap(b'');
- 左侧
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
值,而是将 key
、 val
以及 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 数据结构的主要内容,如果未能解决你的问题,请参考以下文章