精确编程

首页 文章列表 订阅

Github 知乎 豆瓣 Blogger 啄木鸟社区

Postgresql实用技巧 2019-07-11

总结一下最近用到的几个实用的PG特性:

  • domain 创建类型别名

    create domain amount as decimal(33, 18);
    create domain price as decimal(33, 18);
  • 自定义枚举类型

    使用的时候可以传字符串,实际存储是整数。 create type 还可以创建其他更复杂的类型,这里就不详细介绍了。

    create type side as enum ('buy', 'sell');
  • Heap-Only-Tuple 和 fillfactor

    这个和PG的MVCC实现有关系,简单的说,PG的表数据是存在无序的堆上的,包括主键的所有索引都是单独的,索引引用数据行在堆上的位置。 加上MVCC的实现,Update其实是Insert+Delete,新的行数据在堆上的位置发生改变,使得需要更新索引数据。为了优化这种情况, 如果在该条数据相同的Page上面有空闲位置,新插入数据优先放在相同页上,并和旧版本数据建立链表。在查询数据的时候,根据这个链表再去处理事务可见性等判断。 这样就不需要去更新索引数据了。如果Page上没有空闲位置就没办法了。所以 create table 的时候可以指定 fillfactor 选项,预先留出一些位置。 适合数据量不大且更新频繁的场景。

  • 用snowflake算法生成唯一ID很实用,但是在PG上有个问题,PG遵循SQL标准,不支持无符号64位整数。snowflake留了41bit给毫秒级时间戳,去一位给符号位,剩下40位。 算一下发现2004年就溢出了,其实就算无符号64位整数,2039年也就溢出了。解决方案,时间戳统一减去一个最近的时间点,瞬间多出50年。 在PG里面用这种ID,结合自定义函数,连时间戳字段都省了(PG12貌似要支持虚拟字段了)。自动分表也可以按照ID进行,达到按时间戳分表一样的效果,还能保留主键功能,一举多得。

  • 批量insert/update/upsert/delete/move

    数据库里面,批量操作的吞吐量和单条执行不在一个数量级。insert批量大家都知道:

    insert into table values (1,3,3), (2,3,3), (3,3,3)...

    用psycopg2的朋友需要注意的是, cur.executemany 函数并不是真批量操作,他还是生成一堆独立的insert执行,真正的批量操作要这么写:

    psycopg2.extra.insertvalues('insert into table values %s', [(1,3,3), (2,3,3), (3,3,3)])

    update 如何批量呢:

    with tmp(id, name) as (values (1, 'a'), (2, 'b'), (3, 'c'))
    update table set name=tmp.name from tmp where table.id=tmp.id;

    upsert:

    insert into table values (1,3,3), (2,3,3), (3,3,3)...
    on conflict (a) do update set a=table.a+excluded.a

    delete:

    delete from table where id = ANY[%s]

    move, 有时候我们想把数据批量从一个表移动到另一个表:

    with moved_rows as (
    with tmp(id, a, b) as (values %s)
    delete from t_old_table a
    using tmp
    where tmp.id=a.id
    returning a.*
    )
    insert into t_new_table select * from moved_rows
  • mvcc snapshot。问题:如何把一个并发访问的表一致的分割成多段,比如我们想要分批次对里面的数据进行处理,但不希望漏数据,也不希望重复处理。

    注解

    自增ID?在并发访问下,自增ID并不能保证连续没有间断的,比如中间可能还有事务没有提交。

    解决方案是,在表里保存 txid_current() ,要分段的时候,记录下 txid_current_snapshot() ,snapshot由三个信息组成:xmin, xmax, xip。 xmin是当前进行中的事务ID中最小的,也就是比xmin更小的事务都已提交或者撤销,xmax是下一个将要分配的事务ID,比xmax更大的事务还没出现,xip是当前进行中的事务ID列表。 那么一条记录是否发生在这个snapshot之前就很明确了, txid < xmin or (txid < xmax and txid not in xip) ,对应的函数是 txid_visible_in_snapshot(txid, snapshot) 。

  • pipelinedb,流式聚合

    create foreign table trade (time timestamp with time zone, price decimal, amount decimal) server pipelinedb;
    create view kline_1m as select
    date_trunc('minute', time) as time,
    keyed_min(time, price) as open,
    max(price) as high,
    min(price) as low,
    keyed_max(time, price) as close,
    sum(amount) as volume,
    sum(amount * price) as value,
    count(*) as count
    from trade group by 1;
    create unique index kline_1m_time_idx on kline_1m(time);

    然后只管疯狂对 trade 表insert, kline_1m 会自动更新聚合数据,而且 trade 不会保留数据。

  • timescaledb,时序数据库,按照时间戳字段全自动分表。虽然pg有了声明式分表,但还是不如全自动来的爽。再也不用担心表数据量过太多影响性能了。

其他的留到下篇再介绍。

数字货币交易系统架构设计 2019-06-29

交易系统是一个读写请求频率都很高的系统,优化读请求比较轻松,优化写请求是难点,往往很难做到无限扩展。 首先尽可能按照业务逻辑进行分区,其次要兼顾持久化、数据一致性和性能各方面的要求,甚至要在其中进行权衡,比如数据的最终一致性。 好在业务层面大家已经习惯了异步的处理机制,发起请求和请求执行结果是异步的,这使得我们可以采用流水线架构提高吞吐量。

流水线

  1. 下单

    下单是个无状态的Web服务,冻结资产成功后把请求转发给撮合引擎。

  2. 撮合引擎

    撮合引擎需要维护一个持久化的OrderBook。撮合后输出成交记录和订单状态变更。

  3. 清算

    根据成交结果修改用户资产。

  4. 行情分发

    根据成交记录计算K线、深度、最近成交等行情数据。

撮合引擎首先可以按照币对分区,但是单个币对的请求基本上需要顺序执行。具体实现用关系数据库(比如postgresql的plpgsql)吞吐量可以做到每秒上万比成交,并且持久化和可用性方面都不用担心。 性能上要再上一个数量级只能自己在内存实现,需要自己处理好数据持久化,容错容灾等机制,一般来说每秒几十万比成交都还比较轻松。

清算的关键点在于多币种资产系统的设计,多纬度资金对账,在确保资金安全的前提下提升性能。设计上可以参考复式记账法,对系统钱包、手续费账户等热点系统账户特殊处理。

行情分发里面K线的处理稍微麻烦点,逻辑上K线就是对成交记录的一次聚合查询:

select time_bucket(time, '1 minute') as time,
first(price order by time) as open,
max(price) as high,
min(price) as low,
last(price order by time) as close,
sum(amount) as volume,
sum(amount * price) as value
from trades group by 1;

但是不停的全量扫描肯定不行,使用continuous aggregates机制,持续的对增量数据进行聚合,一些专门的时序数据库都支持这种操作,postgresql的 timescaledb 扩展也可以。 另外只有1分钟的k线需要直接从成交记录里聚合,其他k线可以从低纬度的k线数据聚合而成。

系统整体性能取决于短板,短板往往出现在数据库操作并且数据量大的时候,成交记录和订单记录的插入和变更,对于这种插入为主的时序数据,使用 timescaledb 可以轻松处理上百万tps。

看到很多因为使用方式不佳造成关系数据库性能底下的案例,比较可惜,现在Postgresql的可玩性已经很强,尤其大量扩展,搭配其他组件一起使用,应对大部分的业务压力都是问题不大的,同时又能保留关系数据库的便利。

本人目前自由职业,可以提供技术方面的咨询服务,有这方面需要的朋友,欢迎联系。

UTxO模型下的智能合约 2018-12-17

大家都熟悉了以太坊风格的智能合约。以太坊的智能合约首先是基于账户模型,每个合约有一个自己独立的账户地址,这个账户地址可以存储合约持有的资产,也可以存储合约的状态。 以太坊基于这个持久化的状态存储,把智能合约设计成了命令式编程语言的模式,就像传统客户端服务器编程一样,链下代码调用链上代码暴露的API,对合约状态直接进行修改。

UTxO和账户模型各有特点,这里不一一列举彼此的优劣,但在支持智能合约的状态存储方面,账户模型确实似乎更方便一些。 但UTxO模型也不是无法实现,一方面比特币早就支持了有限的脚本功能(脚本就可以用来创建智能合约),我们需要考虑的是如何扩展比特币风格的脚本能力,使其能和以太坊风格的智能合约在功能上等价。

比特币风格的脚本和以太坊很不一样,比特币的脚本其实只做一个校验的工作。比如说你要花费一个普通UTxO,你需要提供地址对应的公钥以及签名,这笔交易就能被校验通过。当你要花费一个脚本UTxO的时候,你需要提供对应的脚本源码和参数,由脚本运行结果决定你这笔交易的合法性。这里有一个特点就是,链上脚本只提供校验功能,可以理解为一个只返回bool类型的函数,而不像以太坊合约可以直接返回计算结果。所以要利用脚本做一些计算,需要和链下代码有更复杂的配合,比如如果要通过动态计算决定output的value的话,需要先在链下计算好结果,然后构造交易并广播,链上代码再验证你提交的结果的正确性,等于实际逻辑需要在链下和链上各执行一遍。而以太坊模式下面,链下代码和链上代码的关系基本上就是传统的客户端服务器的关系。这个倒只是编程模式上的区别,功能上还是一样的。最主要的还是缺乏状态存储,使得比特币脚本无法用来实现让以太坊大放异彩的ICO和编写Token的能力。

Cardano准备引入一个Extended UTxO的模型,就是在output里面增加一个data script的字段,可以用来给UTxO的脚本提供状态存储的能力。

Cardano本身其实就是个采用PoS的比特币,和比特币一样基于纯粹的UTxO模型,Cardano专门设计了一门函数式编程语言Plutus来代替比特币脚本来做智能合约的开发。 Cardano的脚本UTxO里面会涉及三个脚本字段: validator script, redeemer script和data script。 其中validator script就和比特币脚本的职责一样,校验当前交易的合法性,redeemer script其实对应比特币脚本的参数,在这里表现为一个Plutus表达式, data script是新增的字段,以前output只有地址和Value两个字段,现在增加一个data script字段,也是一个Plutus表达式,可以表达任何值。

举一个ICO的合约为例,对于以太坊来说,大概就是一个map存储每个人的地址及其转入的金额,三个接口:投资(contribute)、融资成功后把钱转走(collect)、融资失败后用相同的地址请求退钱(refund),不同的角色在不同的时机去调用不同的接口即可。

在UTxO模型下实现这个功能大概流程是这样的:

  • 发起人创建一个合约脚本,并公布它的脚本地址。
  • 投资人往这个地址转账(contribute),也就是创建这个脚本的UTxO。
  • 这个脚本执行的时候接受一个参数,表示这次调用行为是collect还是refund。
  • 发起人发现融资成功条件满足后,构造一笔交易,给脚本传入collect参数,花费该地址所有的UTxO转到自己账户去。 脚本可以检查发起人的身份,融资行为成功的条件等等。
  • 如果融资失败后,投资人请求refund,给脚本传入refund参数,花费自己创建的那个UTxO,转到自己账户去。 这个时候脚本要检查调用者身份的时候存在一点问题,UTxO里面的数据只有脚本地址和金额,并没有当时转入人的账户信息。 这个时候我们可以利用前面说的data script字段,在contribute阶段,给自己创建的那个UTxO添加一个共钥数据。脚本就可以校验当前交易发起人和UTxO中保存的共钥是否匹配了。

可以看出UTxO合约的编程模式和以太坊还是挺不一样的,利用data script存储状态就像是函数式编程利用递归来保存中间状态的感觉, 具体优劣可能还需要更多的实践才知道。感兴趣可以看一个完整的 Plutus Tutorial 是什么样的。

轻钱包和Cardano 2018-08-19

有时候用户体验和安全性/去中心化/隐私性等特性是冲突的,比如大家都想要的轻钱包。

不过首先要声明我们这里指的轻钱包不是所谓的交易所钱包或者服务器钱包,我还是认为钱包的底线依然是:私钥必须加密存放在用户的设备上, 并且代码应该开源,使得大家可以验证这一点,具体大家可以参考前一篇: 关于钱包的安全性 。

比特币PoW机制利用SPV机制算是比较好的解决了轻钱包的问题,只需要同步几百M数据,同时可以保留和全节点钱包类似的安全性和隐私性。 其中根本原因还是因为PoW协议的简洁性,共识协议相关的信息(算力)已经包含在哈希值里面了,只需要拿着区块头就可以确认链数据的正确性。 而且比特币一个区块头只需要80个字节。作为对比以太坊的区块头是500多个字节,Cardano是600多个字节,而且Cardano目前的共识协议相关的验证所需要用到的MPC中间数据,还不在区块头中, 意味着类似SPV的轻钱包模型,基本上没有可行性。

注解

Cardano区块头格式(n表示一个整数,编码长度可变)

protocol magic : n
hash(prev header): 32
body proof
  tx proof
    number : n
    merkle root: 32
    hash(witnesses): 32
  mpc proof
    hash(data): 32
    hash(vss certificate): 32
  hash(delegate payload): 32
  hash(update payload): 32
consensus data
  slotid: (n, n)
  leader public key: 64
  difficulty: n
  signature: 64 * 4
extra block data: 0

当然,在Cardano去中心化之前,不去校验区块数据其实没有太大问题,因为目前数据来源本来也都是官方的服务器,不存在信任问题。 如果Cardano去中心化后能够实现Ouroboros Praos,轻钱包问题很可能还会有更完美的解决方案。

OK,暂时选择不纠结数据校验的问题之后,就只剩下几个问题需要考虑:

  • 钱包恢复

    钱包恢复操作的本质就是在链上找出属于这个钱包的所有地址。 目前Cardano官方钱包生成地址的索引不是按顺序来的,这导致恢复钱包需要检查链上所有地址,这个是目前官方钱包恢复钱包操作慢的主要原因。 这个问题可以通过实现 BIP44 规范解决,生成新地址的索引是连续的。在恢复钱包的时候,只要按顺序检查每个地址在链上是否存在, 直到遇到第一个链上不存在的地址时(或者允许少量的空间),就可以认为恢复完成了。 但是要升级到BIP44规范不是一个向后兼容的操作,所以很可能新钱包不能支持大家直接恢复目前官方钱包里面的钱包。 但是大家依然可以使用原来的助记词创建新钱包,然后在目前官方钱包上做一笔转账,就可以转换到新钱包上来。

  • 下载数据

    轻钱包的关键就是要尽量减少需要下载的数据。在放弃校验数据的情况下,我们首先可以把区块中大部分的数据移除,只需要保留区块头中少量数据用于处理可能的分叉和回滚, 以及区块体中的交易数据本身。其次我们希望尽可能只下载我们自己钱包相关的数据,而不是把所有人的交易数据都下载下来。一个naive的实现方式就是, 把自己钱包的地址集合传给服务器,然后让服务器过滤掉不相关的信息,但是这样就彻底破坏了UTxO模型的隐私保护的优势了。

注解

UTxO模型的隐私性

比特币(以及Cardano结算层)的UTxO模型相比以太坊的账户模型,一个很大的优势就是更好的隐私保护,因为UTxO模型可以做到每次交易都使用新的地址,这样别人知道你一个地址, 没办法关联到你其他地址,所以他只能在链上查询到你这一个地址的交易历史,而无法查看你整个钱包的交易历史。当然根据地址与地址之间转账的情况, 有时候可以猜到一些地址之间关联性,所以这种隐私保护也不是绝对的,但是毕竟比没有强。但是如果把自己的地址集合全部发送给服务器的话, 也就完全暴露了自己所有地址的隐私信息了。

那么有没有可能既保护好隐私,有可以少下载一些数据呢,其实比特币的SPV轻钱包早就有解决方案了,也就是所谓的bloom filter技术(BIP37)。 它会把你的地址集合哈希(哈希表的哈希,而不是密码学的哈希)到一个bitmap的索引,你把这个bitmap传给服务器,服务器下发所有匹配这个bitmap的信息下来, 因为碰撞的存在,里面会有部分来自其他人的信息,本地再做一层过滤就可以了。你还可以调整这个bitmap的大小,在隐私性和数据下载量之间权衡,bitmap越大,隐私保护越好, 但是数据下载量也会增大。

这是我目前能想到的针对Cardano轻钱包最好的实现方式了,官方最近也发布一个开发中的轻钱包项目,目前看代码直接采用上传地址集合的做法, 问题确实简化了,但我认为哪怕是在目前中心化的阶段,也不应该直接放弃隐私性的考虑。

欢迎关注 小艾钱包 的 开发进展 。

关于钱包的安全性 2018-08-09

由于数字货币相关的一些概念和传统的不太一样,许多不搞技术的同学对于如何有效保护自己数字资产的安全,还是有点搞不清楚。希望本篇可以对非技术同学说清楚钱包的安全问题。

钱包本身只是一个比喻的说法,而且可能是很有害的一个比喻,因为实际情况跟传统世界的钱包隔了十万八千里。

打一个更恰当一点的比方:

  • 你的地址就像是一个上了锁的存钱罐,别人可以往里面放钱,但是不能随便拿钱走,谁有钥匙谁就能拿钱走。
  • 存钱罐和钥匙是应该离线生成的,并且是一对一的,你生成一对存钱罐和钥匙后,把存钱罐公布给别人,别人就可以给里面转账。谁拥有这个钥匙,谁就能花这个钱。

注解

助记词

现在大部分钱包在生成密钥对的时候都是符合统一规范的,助记词就是这个规范的一部分。根据助记词可以推导出你的钥匙,所以只需要保存自己的助记词,就等于是保存了自己的钥匙。

私钥存放在哪里

所以,你的数字资产的安全性首先取决于这把钥匙是如何保管的,所以使用钱包时,第一个需要确认的就是钥匙被存放在哪里。 比如有些人把交易所当钱包,这个时候钥匙就是在交易所的手上。还有一些服务器钱包,你的钥匙实际上也是放在钱包服务商的手上。这些情况下,实际上你都是把自己的钱托管给了别人,安不安全就看对方人品了。

还有一些闭源钱包,比如imtoken以及其他一些,虽然他们说钥匙只会存放在你的设备上,但是理论上来说,不检查他的源代码,我们是无法确切地知道他说的是不是事实的。

如果真的想要确保数字资产的安全,首先钥匙要保管在自己手上是最基本的,而且为了确保这一点,你最好使用一个开源的并且有一定声望的钱包。

私钥是如何被盗的

确保钥匙保存在本地的情况下,接下来的风险就是钥匙被盗了。如果保存钥匙的设备(电脑或手机)感染了病毒,那么你的钥匙就有可能被别人拷贝走。

所以大部分钱包都会对钥匙的存储进一步做加密处理,这些钱包会提示你设置一个密码,这个密码就是用来对你的钥匙进行加密,你本地存储的是加密后的版本。就算病毒或黑客盗取了你的密钥文件,在他不知道你密码的情况下,也无法使用你的钱。(所以如果你忘记了这个密码,也不用太担心,只要你备份的助记词还在,依然可以把私钥重新恢复出来)

对密钥做加密有一个微妙的地方需要注意就是,不要在内存里留下明文密钥的痕迹,这一点其实很多钱包可能并没有考虑地这么细致。 因为在钱包运行过程中,当私钥刚创建出来或者被使用的时候,私钥的明文是必然会在内存里出现的,而传统编程的时候,对于不再使用的内存,通常的做法是不管它,这样就很容易把明文的私钥在内存里留下痕迹,给黑客盗取私钥提供了可能性。正确的做法是在使用完后要主动把私钥明文对应的内存区域清零。

其实只要这几点做到位了,你的私钥的安全性就有了基本的保证。就算钱包软件其他地方有漏洞,或者你的系统本身有漏洞导致被病毒或黑客入侵,你的私钥也不会立刻被泄漏出去。 要做到整个软件完全没有安全漏洞可能比较难,但是把上面几点做到位,只要有心,一般还是问题不大的。

冷钱包

但是终极的确保私钥安全性的办法,还是要从一开始就保证私钥不触网,也就是冷钱包的概念。找一台不联网的干净电脑,或者一个开启飞行模式的干净手机,把私钥创建在上面,并且这台设备以后也永远不联网,或者干脆创建后把私钥删掉(如果你只存钱很少花钱并且备份好了助记词的话) 因为查询余额以及收款的操作是不需要私钥的,只有花钱的时候才需要私钥。 这甚至可能比硬件钱包还要安全,毕竟硬件钱包还取决于硬件本身设计有没有bug。

私钥以外的安全性

私钥以外的安全性,主要就是交易数据正确性的风险,也就是双花的风险了。虽然公链项目团队可能花了大力气去设计共识算法保证链上数据的一致性,但是如果钱包这临门一脚撂了挑子,还是白搭。

所谓双花风险就是,虽然你在钱包上看到对方给你转账成功了,但是等你给对方发货后,发现那笔转账是有问题的。

从这个维度来说,又可以把钱包分为几种类型:全节点钱包、轻钱包、连轻都算不上钱包。这几种钱包模式的区别就在于他们如何获取区块链上的交易数据。

  • 全节点钱包是最传统最安全的模式,它会把整个区块链数据下载下来,然后从里面找出和你有关的那些交易显示出来,并且数据同步的时候也会同时监听多个节点,只要其中有一个节点是诚实的,你都不会被骗。
  • 轻钱包是对全节点钱包的优化,它只需要下载所有区块头,以及和你有关的交易信息,可以忽略链上大部分数据。因为有了区块头就可以验证数据合法性了,再加上多连几个节点交叉确认后,安全性其实并不会打折扣。唯一的弊端是隐私问题,不过鉴于以太坊这么火,估计一般用户对这个问题也没那么感兴趣,就不多说了。
  • 还有一些所谓的轻钱包,其实是没有上面的安全性保证的,它连区块头都不下载,直接从服务器获取余额和交易历史等信息,并且通常是只连开发商自己的服务器,这种模式下等于是完全信任钱包开发商服务器提供的数据了,个人认为不能算是去中心化的钱包,虽然它私钥可能还是存放在设备上而不是服务器上。

Cardano的钱包

Cardano的官方钱包因为一些细节的技术决策上的问题导致体验不太好,一直被大家所诟病,不过最近版本更新改进不少。

我自己在阅读Cardano实现的过程中,有时候也会产生一些自己的想法,于是写了一些代码 python-cardano ,目前已经基本能够实现命令行界面的钱包功能,私钥加密部分是直接复用的官方代码,加上目前Cardano本身还没进入去中心化阶段,区块数据都来源于iohk的服务器,所以基本的安全保障还是有的。 不过项目代码整体还处在开发阶段,所以目前只建议用来学习研究,不要实际用它管理资产。

目前这个项目已经能够通过命令行界面提供一些官方钱包还没有的特性,比如:

  • 大大降低的磁盘空间占用,目前把整个链数据同步下来只占用2.5G,还没做压缩,加上适当的压缩估计还能进一步降低一些。官方的1.4版本也会对磁盘空间占用过大问题进行优化,到时候可以对比对比 ;D
  • 支持sign message功能,也就是可以对任意消息进行签名。这样你可以把签名发送给其他人来证明你确实拥有某个地址的所有权, 比如有些群有持币准入门槛的,这个特性就有点用了。
  • 短地址编码格式,通过优化一些编码的开销,可以做到ADA地址的长度非常接近比特币和以太坊地址的长度,比如: 12MM1pbyTk2WuZEnfiicX9gHF4YtFYL8ebUkr1hp ,已和官方沟通,他们也有计划优化地址长度。 另外等delegation上线之后,地址里包含的信息还要增加一些,所以最终还是很难仅仅通过优化编码方式保持这么短的地址。

下一步计划是基于它开发ADA手机钱包,未来这个项目可能也可以作为Cardano侧链生态中的一个开发平台,目前也可以作为大家学习研究的一个基础。有兴趣的朋友可以加电报群一起探讨: Safe ADA Wallet 。

查看全部文章>>


Website content copyright © by 黄毅. All rights reserved.