“假设有两个百万富翁,他们都想知道谁更富有,但他们都想保护好自己的隐私,谁都不愿意让对方或者任何第三方知道自己真正拥有多少财富。那么如何在保护好双方隐私的情况下,计算出谁更有钱呢?”
这是2000年图灵奖得主姚期智院士在1982年提出的“百万富翁”问题。这个烧脑的问题涉及这样一个矛盾,如果想比较两人谁更富有,两人似乎就必须公布自己的真实财产数据。但是两个人又都希望保护自己的隐私,不愿让对方或者任何一个第三方知道自己的财富究竟有多少,那么在普通人看来,这几乎是一个无解的悖论。
然而在专业学者眼里,这是一个专业的加密学问题,可以表述为“一组互不信任的参与方之间,在需要保护隐私信息以及没有可信第三方的前提下进行协同计算的问题”。这也被称为“多方安全计算”(Secure Multiparty Computation,简称MPC或SMC)问题。姚期智院士在提出“多方安全计算”概念的同时,也提出了自己的解决方案混淆电路(Garbled Circuit)。
接下来本文就来介绍一下如何使用混淆电路开源框架Obliv-C解决百万富翁难题。
1. 开发框架Obliv-C
Obliv-C是美国弗吉尼亚大学(University of Virginia)安全研究小组(SECURITY RESEARCH GROUP)的研究项目。它是一款GCC包装器,该框架作者在C语言的基础上对其进行了一定的类C语言处理,添加了一些规则限制来实现混淆电路。Obliv-C支持两方的半诚实敌手方安全模型,源码采用商业友好的BSD许可证公开在GitHub中(https://github.com/samee/obliv-c),读者可从GitHub下载Obliv-C使用。该框架简单实用,下面对该框架进行介绍和使用。
通过docker构建环境
为了方便读者快速试用,这里列出用于构建Obliv-C的docker镜像的代码供读者参考,如代码清单1所示。
代码清单 1 用于构建Obliv-C运行环境的docker镜像的代码
FROM ubuntu:20.04
WORKDIR /root
RUN apt-get update && apt-get install -y \
ocaml \
libgcrypt20-dev \
libgmp-dev \
ocaml-findlib \
opam \
m4 \
git \
vim \
gcc \
make
RUN git clone https://github.com/samee/obliv-c
#如果访问github速度慢,可以使用这个加速地址:
#RUN git clone https://github.com.cnpmjs.org/samee/obliv-c
WORKDIR /root/obliv-c
RUN opam init -a --disable-sandboxing && \
opam switch create 4.06.0 && \
opam switch 4.06.0 && \
eval `opam config env` && \
opam install -y camlp4 ocamlfind batteries bignum ocamlbuild && \
./configure && make
#可以将宿主机中的代码mount到容器的projects目录,使用容器进行编译和运行
VOLUME ["/root/projects"]
WORKDIR /root/projects
#为了后续测试方便特此安装了一些网络工具
RUN apt-get update && \
apt-get install -y iputils-ping && \
apt-get install -y telnet && \
apt-get install -y net-tools && \
apt-get install -y tcpdump
其实即使不使用docker,直接在Ubuntu系统中安装也非常简单,只需安装相应的依赖并下载源代码编译即可。Obliv-C在GitHub上的开源项目有相关安装说明,读者也可以根据代码清单 1推断出安装步骤,这里不再赘述。
注意:GitHub是境外的网站,在境内访问时经常出现网络连接问题。在构建docker镜像时如果访问github速度慢或者网络连接不稳定,可以使用https://github.com.cnpmjs.org这个代理地址。读者如果有能稳定连接GitHub的网络,还是建议使用GitHub官网地址。
然后使用如下命令编译docker镜像:
docker build -t obliv-c .
接下来介绍一下Obliv-C的编程语法和相关规则。
obliv修饰隐私输入数据
任何依赖于隐私输入的(指只有数据拥有方才知道其具体值,不对其他参与方公开的。下面统一以“隐私输入数据”代指此类数据)变量都应该使用obliv修饰符来声明。比如下面声明的函数中变量a依赖于隐私输入数据,而变量b是各方都知晓的公开数据,其返回结果也依赖于隐私输入数据,需要使用obliv来修饰:
obliv bool compare (obliv int a, int b) {
return a < b;
}
使用obliv修饰符修饰的相关规则:
规则1:只有C语言中的基础类型可以使用obliv进行修饰,比如int、char、float等。注意struct和指针也是不被支持的,但是struct中包含obliv字段或者指针指向obliv变量是可以支持的。另外函数也是可以用obliv修饰符修饰的,这部分会在下面进一步说明。
规则2:任何由obliv变量和非obliv变量组合而成的表达式最终也被视为obliv变量。
规则3:非obliv变量可以隐式地转换成obliv变量,但反过来只能是在各方同意调用 revealObliv 系列函数才可以,比如代码清单2所示。
代码清单 2 revealOblivInt使用示例
int a = 50, b;
obliv int c;
c = a; // 可以,非obliv变量a可以隐式地转换成obliv变量b
b = c; // 不可以, obliv变量c不可以直接转换成非obliv变量
revealOblivInt (&b, c, 0); //ok,使用revealObliv函数公开
使用revealObliv函数后,变量b就是一个普通的整型数,其值与变量c相同。上面代码中revealOblivInt中的第三个参数用来指定接收公开后变量的接收方,如果传入0则表示调用各方都会收到变量c的拷贝值并赋值到变量b。如果传入1,那么只有1号参与方才能收到变量c的拷贝值,而其他参与方只能收到一个默认值0。而这里的参与方编号则是各参与方通过调用setCurrentParty函数进行设定的,比如代码清单3根据程序运行时传入的命令行参数的不同将两个参与方分别设为1号和2号。
代码清单 3 setCurrentParty使用示例
ProtocolDesc pd;
const char* remote_host = (strcmp(argv[2], "--")==0?NULL:argv[2]);
setCurrentParty(&pd,remote_host?2:1);//设置自己的编号
顾名思义,revealOblivInt是用于公开整型数的函数。根据需要公开的变量类型的不同,revealObliv系列函数还有revealOblivBool、 revealOblivChar、revealOblivShort、 revealOblivLong、 revealOblivLLong、 revealOblivFloat、 revealOblivBoolArray、 revealOblivCharArray、revealOblivIntArray、revealOblivShortArray、revealOblivLongArray、 revealOblivLLongArray。
提供隐私输入数据
那么参与方如何提供obliv数据呢?Obliv-C提供了一系列函数,以整型数为例,可以通过下面的feedOblivInt函数将参与方本地的明文整型数转化成obliv int:
obliv int feedOblivInt (int value, int p)//value:明文整型数,p:参与方编号
feedOblivInt被调用时只会加载本方数据,如果编号p与执行方编号不同,则该函数会被忽略。类似地,根据变量类型的不同,feedObliv系列函数还有feedOblivBool,、feedOblivChar,、feedOblivShort、 feedOblivLong、 feedOblivLLong、 feedOblivFloat、 feedOblivBoolArray、feedOblivCharArray、 feedOblivIntArray、 feedOblivShortArray、 feedOblivLongArray、feedOblivLLongArray。
计算过程中的流程控制
正如上面所述,隐私输入数据只有在调用revealObliv系列函数后才能揭示其具体值。任何在计算过程中出现的中间状态也都是对各参与方隐藏的,这样的话类似while、for等循环流程控制就会无法使用obliv变量。
提示:因为每一个参与方都要执行Obliv-C编制的混淆电路的协议,各个参与方都知道循环执行所花的时间以及循环中迭代执行的次数,所以如果框架允许在循环流程控制中使用obliv变量,那么就会导致数据泄露。因此,在其他一些隐私计算框架中,这个限制往往也存在。
规则4:Obliv-C不支持任何obliv变量被用到类似for、while等循环流程控制语句中。
但是,例外的是Obliv-C支持“obliv if”,其语法结构与普通的if非常类似,如代码清单4所示。
代码清单 4 obliv if语法结构
obliv if (…) {
…
} else obliv if (…) {
…
} else {
…
}
在这里的if条件判断中允许使用obliv变量。然而,需要特别注意的是,在执行的时候任何一个参与方都无法获知其条件判断语句是true还是false。Obliv-C不论其条件是否为true都会执行相应的代码块。比如下面这段代码:
obliv int x, y;
…
obliv if (x < 0) y=10;
不论x是正数还是负数,Obliv-C都会执行一段代码,同时确保任何参与方都无法知晓y的值是否发生了变化。如果x为负数,y将被修改为10。如果x不为负数,则y值不变。y是obliv变量,任何一个参与方都不会知晓其具体的值。
区别于普通的if语句,obliv if有一些特殊的限制。
规则5:在obliv if的语句块中,不能对在其之外声明的非obliv变量进行赋值(因为这可能导致信息通过非obliv变量泄露出去),但是对在其之内声明的非obliv变量进行赋值是合法的。
非 obliv变量在obliv if语句中的赋值示例如代码清单5所示。
代码清单 5 非 obliv变量在obliv if语句中的赋值示例
obliv int x;
int y = 10;
obliv if (x > 0) y = 20; //非法,y不是obliv变量,不能在obliv if语句块中被赋值
obliv if (x > 0) {
//合法,非obliv变量i是在obliv if语句块中被声明的
for (int i=0; i<10; i++) {…}
}
规则6:obliv if语句块内不能执行普通的函数(防止因为普通函数泄露信息),只能执行obliv函数。
上面提到Obliv-C不支持任何obliv变量被用到类似for、while等循环流程控制语句中。但是Obliv-C支持在循环体内执行obliv if语句,因此,假设有obliv变量n,下面的循环语句:
for (i = 0; i < n; i++) {…}
可以改写成如下的形式:
for (i = 0; i < MAX_BOUND; i++) {
obliv if (i < n) {…}
}
显然,通过上面的改写,for循环会固定迭代MAX_BOUND次数,不会泄露obliv变量n的信息。
obliv函数
obliv函数声明方式如下:
void func() obliv { …}
规则7:非obliv函数不能在obliv if或者obliv函数体内被调用。
规则8:obliv函数内部不能对在其之外声明的非obliv变量进行赋值修改。
另外,对于函数引用传参也需要注意。比如下面这个函数func被调用时对于p1指针指向是有限制的(p2是常量指针,不用担心其泄露数据,所以可以引用外部变量),p1指针只能指向obliv if语句块内声明的变量,如代码清单6所示。
代码清单 6 函数引用传参时指针指向限制示例
void func (int* p1, const int* p2) obliv {…}
int x, y;
obliv int a;
//非法,p1指针指向了外部声明的变量x
obliv if (a < 0) { func(&x, &y); }
//合法,p1指针指向了obliv if内部声明的变量i
obliv if (a < 0) { int i; func(&i, &y); }
对数组的访问
规则9:obliv变量不能被用于数组索引(Obliv-C开发者认为虽然可以实现,但性能太慢)、指针偏移量、或者数字移位运算中的移位次数中。需要注意的是,普通int整型数是可以被用在obliv数组中的索引中的。
那么如果需要根据obliv变量对数组进行访问需要如何处理呢?Obliv-C的作者也给出了解决方法,如代码清单7所示。
代码清单 7根据obliv变量对数组进行访问的示例
void writeArray (obliv int* arr, int size, obliv int index,
obliv int value) obliv {
for (int i = 0; i < size; ++i) {
obliv if (i == index) {
arr[i] = value;
}
}
}
显然,根据obliv变量对数组进行访问的时间复杂度不再是O(1)而是O(n)了。
关键词frozen
Obliv-C引入frozen关键词对变量进行修饰,其含义与const类似。其引入的原因主要是考虑到结构struct类型在某些使用场景下需要是深度常类型(deep-const),比如代码清单8所示的场景,frozen修饰符的作用被递归应用到了变量b内的所有指针从而保证b的内部指针p不会被赋值。
代码清单 8 frozen关键词使用示例
struct S { int x, *p; };
void func (const struct S* a, frozen struct S* b) {
a->x = 5;//非法
b->x = 5;//非法
*a->p = 5;//合法,a->p的类型是 int *const 而不是const int*
*b->p = 5;//非法,frozen是递归应用到了struct/union内的所有指针
}
对于struct、union以及指向指针的指针等,const和frozen存在差异,比如int **frozen与const int *const *const相同,而与const int**或者int **const不同。
规则10:通常情况下,任何非obliv变量在进入obliv作用域(obliv-if或者obliv函数)时,都可以视作被frozen修饰符修饰。
这个规则也比较好理解,因为如果obliv作用域的非obliv变量不是被视作被frozen修饰的话,信息就有可能通过给非obliv变量赋值的方式泄露出去。
规则11:对于任何类型T,frozen指针T *frozen 的解引用(dereferencing)获得的是一个T frozen类型的左值。
规则12:对于obliv数据,frozen修饰符会被忽略。
高级功能:无条件代码段
这部分是Obliv-C与C区别最大之处。无条件代码段是指在obliv条件性代码段中拆分出一块代码段进行无条件执行,如代码清单9所示。
代码清单 9 无条件代码段的示例
int x = 10;
obliv int y;
obliv if (y > 0) {
x = 15;//非法:不能在obliv作用域修改非obliv变量x
~obliv (c) {//开启无条件代码段
x = 15;//合法:c即使为false的情况下赋值仍然会发生
}
}
规则13:无条件代码段的执行不依赖于任何obliv变量值,frozen变量的限制在无条件代码段中不再生效。
但是这里有个地方需要小心,在上面第3行代码obliv if (y > 0)中,即使y不是正数,无条件代码段仍然会被执行(即使在c为false的情况下也会执行)。一般情况,~obliv (varname)语法中也声明了一个obliv的布尔变量varname,该varname可被用在无条件代码段内部的obliv if的条件判断上。示例如代码清单10所示。
代码清单 10 无条件代码段中声明的obliv布尔变量的使用示例
void swapInt(obliv int* a,obliv int* b) obliv {
~obliv(en) {
obliv int t = 0;
obliv if(en) t=*a^*b;
*a^=t;
*b^=t;
}
}
项目代码文件结构
在基本了解Obliv-C的语法后,我们以求向量内积的案例为例,进一步了解使用Obliv-C进行编程时的项目基本结构。读者在刚开始接触Obliv-C项目时,建议同时通过src/ext/oblivc目录下的obliv.oh、obliv.h两个文件来了解Obliv-C提供的接口。然后通过进一步阅读test/oblivc目录下的几个测试案例熟悉接口的使用方法。
实现求向量内积共需4个文件,即innerProd.c、innerProd.h、innerProd.oc、Makefile文件,接下来我们来看一下每个文件的作用。
(1)innerProd.h文件
编程规则与C语言完全相同,本文件用于声明函数、定义混淆电路相关结构体(protocolIO)以及参与混淆计算的全部参数(包括各方的隐私输入以及最后的共享结果)。特别地,各方的隐私输入可以定义为同一变量名,也可以定义为不同的变量。对应的代码如代码清单11所示。
代码清单 11求向量内积innerProd.h代码
#pragma once
#include<obliv.h>
void dotProd(void *args);//混淆计算函数的声明,具体的函数定义见innerProd.oc
typedef struct vector{
int size;
int* arr;
} vector;
typedef struct protocolIO{
vector input;
int result;
} protocolIO;//包含了混淆电路计算输入和输出的相关结构体
注意:对于protocolIO结构体,其中的变量不能使用指针,需要用数组代替,否则编译可能不会报错,但最终运行结果错误。
(2)innerProd.c文件
编程规则与C语言完全相同,本文件程序用于获取命令行参数、设置混淆电路环境、输出混淆计算结果等。其主要执行顺序为:
1)获取并校验命令行参数。
2)与另一个参与方进行网络连接,如代码清单12所示。
代码清单 12 innerProd.c文件中进行网络连接的代码段
ProtocolDesc pd;//混淆电路的相关函数都需要使用这个变量
protocolIO io;
const char* remote_host = (strcmp(argv[2], "--")==0?NULL:argv[2]);
if(!remote_host){//两个参与方进行网络连接
if(protocolAcceptTcp2P(&pd, argv[1])){
fprintf(stderr, "TCP accept failed\n");
exit(1);
}
} else{
if(protocolConnectTcp2P(&pd,remote_host,argv[1])!=0){
fprintf(stderr,"TCP connect failed\n");
exit(1);
}
}
提示:protocolAcceptTcp2P和protocolConnectTcp2P是Obliv-C提供的为两个隐私计算参与方建立连接的接口。
3)设置自己的编号。
int currentParty = remote_host?2:1;
setCurrentParty(&pd, currentParty);//两个参与方分别设置自己的编号
4)从文件中读取向量内容,如代码清单13所示。
代码清单 13 innerProd.c文件中读取向量内容的代码段
vector v;
FILE* file = fopen(argv[3], "r");
if(fscanf(file, "%d\n", &(v.size)) == EOF){//从文件中读取向量大小
fprintf(stderr, "Invalid input file\n");
return 2;
}
v.arr = malloc(sizeof(int) * v.size);
for(int i=0; i<v.size; i++){//从文件中读取向量值
if(fscanf(file, "%d\n", &(v.arr[i])) == EOF){
return 2;
}
}
5)执行混淆电路代码。
io.input = v;
execYaoProtocol(&pd, dotProd, &io);//执行混淆电路代码
6)输出计算结果。
int result = io.result;
fprintf(stderr, "DotProduct is %d\n", result);//输出计算结果
7)清理。
cleanupProtocol(&pd);//固定用法,清理ProtocolDesc pd
(3)innerProd.oc文件
编程规则与C语言类似,用于定义混淆计算函数(即本例中的dotProd函数),相关语法在前面部分已有描述。对应的代码如代码清单14所示。
代码清单 14求向量内积innerProd.oc代码
#include<obliv.oh>
#include"innerProd.h"
void dotProd(void *args){
protocolIO *io = args;//混淆计算参数对应结构体获取
int v1Size = ocBroadcastInt(io->input.size, 1);
int v2Size = ocBroadcastInt(io->input.size, 2);
obliv int* v1 = malloc(sizeof(obliv int) * v1Size);
obliv int* v2 = malloc(sizeof(obliv int) * v2Size);
//获取参与计算的向量,最后一个参数为提供数据的参与方编号
feedOblivIntArray(v1, io->input.arr, v1Size, 1);
feedOblivIntArray(v2, io->input.arr, v2Size, 2);
int vMinSize = v1Size<v2Size?v1Size:v2Size; //如果两方向量长度不同以小的为准
obliv int sum = 0;
for(int i=0; i<vMinSize; i++){
sum += v1[i]*v2[i];
}
revealOblivInt(&(io->result), sum, 0);//揭示计算结果
}
提示:在上面的代码中,ocBroadcastInt函数是用来将非obliv数据传给其他参与方的。根据数据类型不同,类似的还有ocBroadcastFloat等函数。
(4)Makefile文件
本文件用于编译。Makefile文件的编译只需在对应的文件目录打开命令行终端,输入make后回车即可。编译成功产生一个a.out可执行程序文件。运行按相应的格式输入参数便可。对应的代码如代码清单15所示。
代码清单 15求向量内积程序的Makefile脚本
privacyProgram=innerProd
CILPATH=/root/obliv-c
REMOTE_HOST=localhost
CFLAGS=-DREMOTE_HOST=$(REMOTE_HOST) -O3
./a.out: $(privacyProgram).oc $(privacyProgram).c
$(CILPATH)/_build/libobliv.a
$(CILPATH)/bin/oblivcc
$(CFLAGS) $(privacyProgram).oc $(privacyProgram).c -lm
clean:
rm -f a.out
clean-all:
rm -f *.cil.c *.i *.o
至此,相信读者应该对Obliv-C项目的文件结构有了基本的了解,接下来就可以尝试一个小的应用案例了。
2. 应用案例:解决百万富翁难题
具体代码实现
通过上面的介绍,使用Obliv-C来实现百万富翁问题就变得非常简单了。首先在C:\ppct\obliv-c\目录下创建3个文件:million.h、million.c、million.oc。
提示:C:\ppct是本文项目代码放置的目录,读者在代码实践时可根据自己的系统情况修改。
在million.h文件中需要定义隐私输入(即两个富翁的财富值)和输出(即两个富翁中谁更富有)以及隐私计算函数millionaire,具体代码如代码清单16所示。
代码清单 16百万富翁问题million.h代码
typedef struct protocolIO {
int cmp; //隐私计算输出,-1:Alice小于Bob, 0:Alice等于Bob,1:Alice大于Bob
int mywealth;//隐私计算输入
} protocolIO;
void millionaire(void* args);//隐私计算函数
然后在million.oc文件中编写具体的隐私计算函数millionaire(),具体代码如代码清单17所示。
代码清单 17百万富翁问题million.oc代码
#include<obliv.oh>
#include"million.h"
void millionaire(void* args) {
protocolIO *io=args;
obliv int aliceWealth,bobWealth;
aliceWealth = feedOblivInt(io->mywealth,1);//获取隐私输入
bobWealth = feedOblivInt(io->mywealth,2);
bool eq,lt;
revealOblivBool(&eq, aliceWealth == bobWealth, 0);//首先比较两人是否同样富有
revealOblivBool(<, aliceWealth < bobWealth, 0);//然后比较是否Bob更加富有
io->cmp = (!eq? lt?-1:1 : 0);//输出最后比较结果
}
接下来在million.c文件中进入主函数编写,其主要流程为参与方之间建立网络连接、设置自己的参与方编号、输入自己的财富值、执行混淆电路代码进行财富值比较、输出结果并清理。具体代码如代码清单18所示。
代码清单 18百万富翁问题million.c代码
#include<stdio.h>
#include<obliv.h>
#include"million.h"
int main(int argc,char *argv[]) {
ProtocolDesc pd;
protocolIO io;
const char* remote_host = (strcmp(argv[2], "--")==0?NULL:argv[2]);
if(!remote_host){
if(protocolAcceptTcp2P(&pd, argv[1])){ //Alice等待Bob连接
fprintf(stderr, "TCP accept failed\n");
exit(1);
}
}
else{
if(protocolConnectTcp2P(&pd,remote_host,argv[1])!=0){ //Bob主动连接Alice
fprintf(stderr,"TCP connect failed\n");
exit(1);
}
}
setCurrentParty(&pd, remote_host?2:1); //设置参与方编号,Alice是1,Bob是2
sscanf(argv[3],"%d",&io.mywealth); //这里省略输入合法性检验
execYaoProtocol(&pd,millionaire,&io); //执行百万富翁比较
cleanupProtocol(&pd);
fprintf(stderr,"Result: %d\n",io.cmp);
return 0;
}
最后创建Makefile文件,修改一下上文提到的Makefile文件,将其中的privacyProgram值修改为“million”即可。
利用上面的docker镜像,通过以下命令运行Alice方实例:
docker network create obliv-c-net
docker run -it --rm --name Alice --network obliv-c-net `
-v C:\ppct\obliv-c:/root/projects obliv-c
make
提示:上面的代码中因docker run运行的参数较多,为方便阅读使用“`”进行换行处理。这是Windows的PowerShell环境下将命令拆分成多行的特殊字符。在Windows的CMD环境则需要改用“^”字符,在Linux环境则需要改用“\”字符。
另外,为了方便Alice和Bob的两个容器间直接使用机器名进行网络通信,特意创建了obliv-c-net网络用于测试。在实际应用中,两个参与方一般位于不同的机器,直接基于IP或者域名进行通信,不需要创建obliv-c-net网络。
编译成功即可看到同目录下生成了隐私计算执行文件a.out。使用如下命令运行Bob方实例:
docker run -it --rm --name Bob --network obliv-c-net `
-v C:\ppct\obliv-c:/root/projects obliv-c
进入Alice方容器执行以下命令(其中,“1234”为端口号,“--”代表为服务方等待对方连接,“8”代表Alice的财富值):
./a.out 1234 -- 8
进入Bob方容器执行以下命令(其中“1234”为端口号,“Alice”为需要连接的对方服务地址,“15”代表Bob的财富值):
./a.out 1234 Alice 15
最后双方都输出了执行结果-1,即Alice财富值小于Bob。
本文介绍了如何使用混淆电路开源框架Obliv-C解决百万富翁难题。隐私计算技术作为重大科技趋势正逐渐受到政府和企业的重视,更成为商业世界和资本竞逐的热门赛道。了解并掌握隐私计算技术,将成为立足大数据时代的必备技能。如果您想要了解更多有关隐私计算技术的内容,快速了解并上手隐私计算,推荐您详细阅读李伟荣老师的新作《深入浅出隐私计算:技术解析与应用实践》。
关于作者:李伟荣,曾就职于微软、平安、港交所等大型公司,拥有十年以上金融项目架构和信息安全管理经验。精通信息安全、软件研发、项目管理,擅长大型软件架构开发,善于使用创新思维和创新方法解决问题。
曾在港交所深度参与隐私计算相关项目,致力于通过隐私计算技术解决大数据产品的确权、标准化、存证、溯源、定价、信用体系和利益分配等一系列问题,打造数据、金融资产交易的新型基础设施。
视频推荐👇
书讯 | 4月书讯(上)| 上新了,华章
书讯 | 4月书讯(下)| 上新了,华章
资讯 | 分布式系统一致性的本质,看这篇秒懂
干货 | Go语言精进之路:你知道什么是Go语言编程思维吗?
收藏 | 终于有人把Scrapy爬虫框架讲明白了
上新 | Verilog HDL与FPGA数字系统设计第2版
活动 | 【向上突破系列】第1期:互联网裁员潮下,去还是留?
更多推荐