作者:林冠宏 / 指尖下的幽灵。转载者,请: 务必标明出处。

GitHub : https://github.com/af913337456/

出版的书籍:


要了解 Nostr,得从头开始


目录

  • 服务的演变
  • Nostr 的服务形态
  • Relay 的局限性
  • Nostr 与区块链的关系
  • Nostr 协议内容选集
    • 账户部分
    • 数据操作部分
      • Client 发布一个事件
      • tags 参数使用例子
      • Client 发订阅请求
      • Client 取消订阅
      • Client 发授权请求
      • Relay 返回的命令
    • 实操及观察
      • 部署 Relay 服务
      • 使用 Client 与 Relay 交互
      • 查看数据
    • kind 附录
  • 鸣谢

其实 Nostr 的整体思想并不算新颖,提出的时间也是好几年前了。最近这年由于 web3 应用概念的火爆导致它被挖出来并广为人知。

三年前的基于 Golang 的 client ,noscl

服务的演变 [可跳过]

我们固有的 C/S 服务思维是 client 发请求,server 相应结果。

当简简单单的时候,就是如图下面的情况:

特征:低处理量,低可用,数据高内聚

当用户量越来越多,产品做大了的情况,server 就是一个集群,里面有各种小 server。

这种时候的集群服务所有权是集中的,集中于一个组织或多个组织。不够去中心化。

特征:高处理量,高可用,高耦合,数据可分布式存储,仍然中心化

再后面,区块链出现了,此时的服务要走去中心化的线路,服务不再叫做服务,而是节点,说法虽然不一样了,但本质还是提供数据处理、存储、整理返回给 client 的功能。如下图:

这是一种更加复杂的 C/S 通讯模式:

  1. Client 可以与任一个 Node 通讯;
  2. 区块链节点 Node 分布在各地,谁都部署一个 Node Server 然后加入到节点网络;
  3. Node 节点相互 P2P 通讯;
  4. 整个拓扑比例:N:Y:Z

特征:区块链特征

Nostr 的服务形态 [核心]

如果你看了上面 服务的演变 小节,可以思考下,服务的架构的大方向还可以怎么变?我们把区块链节点的的服务图做一些修改变成下面这图:

去掉 Node 之间的 P2P 通讯。变成:

  1. Client 可以与 单个 或 多个 Relay 通讯;
  2. Client 各自独立,不能相互通讯;
  3. Relay 有多个,且分布在不同地方;
  4. Relay 之间没任何通讯,各自独立;

这就是 Nostr 协议所描述的服务框架。在 Nostr 中,没有了 server 的称呼,变成了 Relay。就像区块链一样,把 server 变成了 node。

产生了一个核心问题:

数据如何同步?比如 Client-A 如何看到 Client-B 发的信息/消息?

对于这个问题,如果 Relay 之间有 P2P 通讯,那么就像区块链节点那样,数据相互同步便解决了。但 Nostr 所描述的做法却是这样的:

  1. Client-A 给 Relay-A 发数据,Relay-A 存储在本地;
  2. Client-B 给 Relay-B 发数据,Relay-B 存储在本地;
  3. 因为要交流,否则就是玩单机,于是乎,A 或 B 两个用户,就会:
    1. 在某些中心化社交平台暴露自己的 Relay 的访问 Url;
    2. 用某其他软件私聊互发 Relay 的 Url;
  4. 在 3 的基础上,Client-A 或 Client-B 就可以在 Nostr 协议所实现的 Client 端软件上加载到对方 Relay 存储的数据,也就达到了看见/交流的目的。

Relay 的局限性

用户要使用 Nostr 应用,得满足2个条件:

  1. 下载 Nostr 客户端软件,这个已有,网上可搜;
  2. 订阅 Relay,这个可以:
    1. 自己买个服务器,找份 Relay 代码,编译个可执行文件,启动服务,然后连接进来,就可以发数据到它存起来。然后再去找其他朋友的 Relay 链接;
    2. 去网上找公开的 Relay 链接,订阅进去,从而可以发数据,看数据;

为了解决这些问题,现在 Nostr 衍生出的产品中就有公共 Relay,比如:snort.social,这些 Relay 提供注册功能,所谓的注册就是在他的网站生成密钥对。然后如果用户没钱、没技术自己搭建 Relay 服务器,就可以订阅它的 Relay。

这样的话,所有订阅者都可以在网站页面展示出来,让后来的人看到,等于直接看到其他用户,在这些用户中找好友来进行第一次的谈话。

但是如果没有自己的 Relay 的话,数据其实变相存储在别人的服务器。

技术实现方面,这里要注意一个点:

某一 Client 所有订阅了的 Relay,在 Client 发数据的时候,需要给所有订阅了的 Relay 都发。并非强制,但协议要求。

Nostr 与区块链的关系

Nostr 与区块链关系不大

  1. Nostr 不是某公链的 DApp;
  2. Nostr 没涉及到智能合约;
  3. Nostr 没涉及链上请求;
  4. Nostr 在用户账户部分用了和 BTC 一样的公私钥生成算法;
  5. Nostr 和其他区块链的 DApp 拥有一样的 web3 概念

Nostr 协议内容选集

Nostr 协议就像 Http 协议一样,制定了 C/S 的通讯形式。可以使用任何编程语言去实现。下面我将选择几个有代表性的部分来讲解下,最好是去看协议文档。

官方文档:nostr-protocol

账户部分

Nostr 的客户账号,不需要依赖 Relay,可以在 Client 本地直接生成。就是 BTC 的钱包。

  1. 私钥充当了密码;
  2. 公钥充当了账号

比如这段代码就是生成个 Nostr 客户端账户,和 BTC 的钱包生成一样:

// 完整的,见文章头部 git 项目
// 从私钥获取公钥
func getPubKey(privateKey string) string {
keyb, _ := hex.DecodeString(privateKey)
_, pubkey := btcec.PrivKeyFromBytes(keyb)
return hex.EncodeToString(schnorr.SerializePubKey(pubkey))
} func keyGen(opts docopt.Opts) {
seedWords, _ := nip06.GenerateSeedWords() // 助记词
seed := nip06.SeedFromWords(seedWords)
sk, _ := nip06.PrivateKeyFromSeed(seed) // 私钥
fmt.Println("seed:", seedWords)
fmt.Println("private key:", sk)
fmt.Println("pubkey:", getPubKey(sk)) // 公钥
}

上面代码运行结果:

seed: arrow suspect reunion hire project damp protect comic leopard market repair diet delay direct bid mountain rigid sister moral speed cloud dawn rain vanish
private key: 3e6d9287d017b5ca1a1219b9d403d172f5ee2df74e112e7d890b070939d1fdb4
pubkey: cb6fd58aa73d01f4e7f803ae41f80caabe2d68288f19a231a6e57571db6a1eb4
数据操作部分
  1. client 和 relay 采用 websocket 的协议传输数据;
  2. 数据格式是 Json;
  3. 标准格式是:[命令,参数,参数...]
Client 发布一个事件

发布命令:["EVENT", <event JSON>]

要注意,事件具体要完成什么动作,完全看里面的参数 kind,见下面的kind 附录小节,可见 kind = 1 的时候,代表发送的是短文,等于发文字帖子。

["EVENT",
{
"id": "21e1b711fa6a9741ab7d134d2ea5a2e6ac6c75751386b411c46438118a4c0dd4",
"pubkey": "86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4",
"created_at": 1679112916,
"kind": 1, // 发文字帖子
"tags": [
["e",<event_id>,<other_1>,<other_2>],
["p",<pubkey>],
....
],
"content": "9988cc",
"sig": "883af7863a63d55c207e272707894944ee763810cd0393d344f85dc9a0bc624c4cc1b28ca483008729b56602f4acaad22e084c2b44db02ecc11e238880b8fd62"
}
]
  1. id 就是当前请求的 id,生成方式是对 event 整体内容进行 sha256.Sum256计算得到,这些都可以在文档中得知;
  2. pubkey 就是发送者 sender;
  3. sig 就是整个 event 的签名,防止内容被篡改,在 Relay 端,会对接收到的 event 验签;
  4. kind 直接理解为 event 的类型,告诉 Relay 要达到什么目的;
  5. tags 纯粹的标签数组,为了携带辅助参数来实现功能而设立,下面我列举两个场景来说明这个参数的灵活使用,要注意,这个参数要实现什么功能,是没固定说法的,思维在这里要灵活
tags 参数使用例子
  1. 场景:发布内容引用到其他内容的时候。可以在 tags 中的 e 标签数组内添加其他 event 的 id;
  2. 场景:删除自己所发布的 event 的时候。可以在 tags 的 e 标签中添加想要被删除的 eventId;
  3. 场景:发私信。私信的 kind 是 4,此时 content 是加密的,只有接收方能解密,此时 tags 的 p 标签中,就是要接收私信人的 pubkey
Client 发订阅请求

订阅操作的完成可以达成两个目的:

  1. 只要 websocket 不关闭,只要 Relay 有新的 event 接收到,就会推送到 Client;
  2. 订阅开始的时候,Relay 会对当前的订阅请求返回一次目标 event 数据;

订阅命令:["REQ", <subscription_id>, <filters JSON>]

[   "REQ",
"b326655084f5f1", // 一次性随机生成的请求 id
{
"ids": ["ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3"],
"authors": ["86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4"],
"kinds": [4], // 标明只加载私信的
"#e": ["9c0c22f940bc5e8bc397206a3a3566e01eccf"], // 同时引用了这个推文
"#p": ["86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4"],
"since": 1679112916,
"until": 1679112996,
"limit": 7
}
]

可以看到,REQ 订阅操作的参数和 EVENT 的大同小异,一样具有很强的灵活性。查询的时候所有参数取且操作

  1. ids,指定基于 event 的变化,不指定就是全量订阅;
  2. authors,要订阅这个作者发的,不指定就没这个限制;
Client 取消订阅

对应到订阅,自然有取消订阅,取消后,Relay 不再推送 event 数据。

取消订阅命令:["CLOSE", <subscription_id>]

subscription_id 就是订阅时候的那个 id。

Client 发授权请求

授权请求在 Nostr 中是个更加高度自定义的动作,根据协议描述,这个请求用于拓展 Relay 的一些主观功能。

首先我们知道,Relay 在整个 Nostr 网络中是会存在很多的,那么就不排除有一些 Relay 代表了一些组织,它们可能只向特定的用户开放,比如说你要接入就要付费或者收到邀请码等前提。

Relay 如何实现这些限制呢?答案就是使用 AUTH。

授权命令:["AUTH", <signed-event-json>]

["AUTH", {
"id": "...",
"pubkey": "...",
"created_at": 1669695536,
"kind": 22242, // 固定,必须是 22242
"tags": [
["relay", "wss://relay.example.com/"], // 目标 relay url,会做校验
["challenge", "challengestringhere"] // relay 返回再放进来
],
"content": "",
"sig": "..."
}]

下面我将使用时序图来说明 Client 和 Relay 是如何进行授权动作的。

  1. Client 在和 Relay 建立链接的时候,Relay 如果实现了 AUTH 功能,就需要给 Client 返回个 challenge code;
  2. Client 拿到 challenge code 后得知此 Relay 是要授权接入的,否则一些功能无法使用;
  3. Client 打包 AUTH 请求命令,把发出;
  4. Relay 验证 challenge code 和其他信息,并存储相关数据;

整个 AUTH 的流程是比较简单的。最核心的信息是 challenge code,完全可以理解为验证码,具体怎么去实现这一部分,完全可以由 Relay 自定义。我上面举的例子是现在 demo 源码的做法,现实中是可以但不限于下面的拓展:

  1. 用户在 A 网站购买 challenge code,再去输入;
  2. challenge code 只能用一次,且 10 分钟有效。

challenge code 的验证代码如下,所关联的参数一目了然:

// ValidateAuthEvent checks whether event is a valid NIP-42 event for given challenge and relayURL.
// The result of the validation is encoded in the ok bool.
func ValidateAuthEvent(event *nostr.Event, challenge string, relayURL string) (pubkey string, ok bool) {
if event.Kind != 22242 {return "", false}
if event.Tags.GetFirst([]string{"challenge", challenge}) == nil {
return "", false
}
expected, err := parseUrl(relayURL)
if err != nil {return "", false}
found, err := parseUrl(event.Tags.GetFirst([]string{"relay", ""}).Value())
if err != nil {return "", false}
if expected.Scheme != found.Scheme ||
expected.Host != found.Host ||
expected.Path != found.Path {
return "", false
}
now := time.Now()
if event.CreatedAt.After(now.Add(10*time.Minute)) || event.CreatedAt.Before(now.Add(-10*time.Minute)) {
return "", false
}
if ok, _ := event.CheckSignature(); !ok {return "", false}
return event.PubKey, true
}
Relay 返回的命令
  1. 订阅之后,给 Client 推送数据。["EVENT", <subscription_id>, <event JSON>],id 就是订阅时发过来的 id;
  2. 给 Client 的请求报错或提示。["NOTICE", <message>]
  3. 告知 Client 当前 Relay 所有存储的 event 都已经返回。["EOSE", <subscription_id>]
  4. 告知 Client,此 Relay 需要授权,并返回 challenge_code。
    • ["AUTH", <challenge-string>]
  5. 告知 Client 某事件的结果,成功或失败。["OK", <event_id>, <true|false>, <message>]

NOTICEOK 的区别是:

  1. NOTICE 多用于请求并未完成,比如参数结构、缺少类的错误
  2. OK 也会用于返回错误,但此时请求已经完成,出错在最终结果。比如验签、授权。

例子:

// 验证算法:https://github.com/nostr-protocol/nips/blob/master/42.md
// 主要就是验证 client 的 challenge 是否是 relay 返回的。这个 relay 的实现是随机搞的 challenge
// 拓展的做法就是可以根据更丰富的算法生成 challenge 再返回。比如与 pubkey、时间加密后返回一个
// 特定的,在多少天内有效的,只能这个 pubkey 访问的 challenge 码
if pubkey, ok := nip42.ValidateAuthEvent(&evt, ws.challenge, auther.ServiceURL()); ok {
ws.authed = pubkey
ws.WriteJSON([]interface{}{"OK", evt.ID, true, "authentication success"})
} else {
ws.WriteJSON([]interface{}{"OK", evt.ID, false, "error: failed to authenticate"})
}
实操及观察

这里我将基于 Golang 实现的 Relay 和 Client 来做一下 Nostr 的简单交互演示。具体的项目见文头的项目。

部署 Relay 服务

Relay 项目地址: https://github.com/fiatjaf/relayer

进入到 basic 目录,将 main.go 文件的内容改成下面的:

package main

import (
"encoding/json"
"fmt"
"log"
"time" "github.com/fiatjaf/relayer"
"github.com/fiatjaf/relayer/storage/postgresql"
"github.com/fiatjaf/relayer/storage/sqlite3"
"github.com/kelseyhightower/envconfig"
"github.com/nbd-wtf/go-nostr"
) type SqliteRelay struct {
SQLiteDatabase string `envconfig:"SQLITE_DATABASE"`
storage *sqlite3.SQLite3Backend
} func (r *SqliteRelay) Name() string {
return "SQLiteRelay"
} func (r *SqliteRelay) Storage() relayer.Storage {
return r.storage
} func (r *SqliteRelay) OnInitialized(*relayer.Server) {} func (r *SqliteRelay) Init() error {
return nil
} func (r *SqliteRelay) AcceptEvent(evt *nostr.Event) bool {
jsonb, _ := json.Marshal(evt)
if len(jsonb) > 10000 {
return false
}
return true
} func main() {
runSQLiteRelay()
} func runSQLiteRelay() {
r := SqliteRelay{}
if err := envconfig.Process("", &r); err != nil {
log.Fatalf("failed to read from env: %v", err)
return
}
r.storage = &sqlite3.SQLite3Backend{DatabaseURL: "jdbc:sqlite:identifier.sqlite"}
if err := relayer.StartConf(relayer.Settings{
Host: "127.0.0.1",
Port: "8888",
}, &r); err != nil {
log.Fatalf("server terminated: %v", err)
}
}

上面代码启动后就会启动本地的 Relay 服务,RelayUrl:http://127.0.0.1:8888,它使用 sqlite3 数据库来存储数据,sqlite3 是库里面自己支持了的。如果不使用这个,需要自己实现其他数据库的版本,根据接口函数来实现即可,难度并不大。

使用 Client 与 Relay 交互

Client 可以使用下面简单的例子,直接进行测试通讯。完成发送 event 和 req 订阅命令的功能。

package main

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"time" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/nbd-wtf/go-nostr"
) func main() {
ctx := context.Background()
pubkey := "86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4"
privateKey := "8d137174f49cce2590d3a30f89a9dd9865b319ef3a94e24b37cfa106ff85259c"
// 加载并订阅
// sendREQ(ctx, pubkey, privateKey)
// 发文字贴文
sendEVENT(ctx, pubkey, privateKey, "hello world")
} func newRelay(ctx context.Context) *nostr.Relay {
relay, err := nostr.RelayConnect(ctx, "http://127.0.0.1:8888")
if err != nil {
panic(fmt.Errorf("failed to connect to relay: %w", err))
}
return relay
} func sendREQ(ctx context.Context, pubkey, privateKey string) {
filter := nostr.Filter{
IDs: nil,
Kinds: []int{nostr.KindTextNote}, // 只订阅短文
Authors: []string{pubkey}, // 只订阅这个作者发的
Tags: nil,
Since: nil,
Until: nil,
Limit: 100,
}
req := newRelay(ctx).PrepareSubscription()
req.Sub(ctx, nostr.Filters{filter})
// 在这里接受所有的返回并打印
for event := range req.Events { // Events 会在 unsub 函数内被关闭
bys, _ := event.MarshalJSON()
fmt.Println("receive event:-------", string(bys))
}
} func sendEVENT(ctx context.Context, pubkey, privateKey, content string) {
helloTxtEvent := nostr.Event{
ID: "", // signEventAndCalculateID 中赋值
PubKey: pubkey,
CreatedAt: time.Now(),
Kind: nostr.KindTextNote, // 1 短文
Tags: nil, // 我们没其他的功能,这里 tag 留空
Content: content,
Sig: "", // signEventAndCalculateID 中赋值
}
if err := signEventAndCalculateID(&helloTxtEvent, privateKey); err != nil {
panic(fmt.Errorf("signEventAndCalculateID err: %w", err))
}
if sendStatus, err := newRelay(ctx).Publish(ctx, helloTxtEvent); err != nil {
panic(fmt.Errorf("publish err: %w", err))
} else {
bys, _ := json.Marshal(helloTxtEvent)
fmt.Println(fmt.Sprintf("send event status [%s] event data: %s", sendStatus, string(bys)))
}
} func signEventAndCalculateID(evt *nostr.Event, privateKey string) error {
h := sha256.Sum256(evt.Serialize())
s, err := hex.DecodeString(privateKey)
if err != nil {
return fmt.Errorf("Sign called with invalid private key '%s': %w", privateKey, err)
}
sk, _ := btcec.PrivKeyFromBytes(s)
sig, err := schnorr.Sign(sk, h[:])
if err != nil {
return err
}
evt.ID = hex.EncodeToString(h[:]) // id 赋值
evt.Sig = hex.EncodeToString(sig.Serialize()) // 生成签名信息
return nil
}
查看数据

执行上面 Client 代码 main 函数中的 sendEVENT 可以在控制台看到发送成功:

send event status [success] event data: {"id":"d80f68baee57f2cbb231f44df27ddbf22728b9649083386a44f01cdf159b398a","pubkey":"86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4","created_at":1679144385,"kind":1,"tags":[],"content":"hello world","sig":"fdb4570c73cfded2c8a29b8ca926a5a400b81246b676f5090651949806f3741c40d2df8542c70f6dbdbff4ada3123cb285fcdcb0fdf499b2bd41a97ed5f4f3bb"}

再执行 sendREQ 函数,进行订阅可以前面发送过的数据:

receive event:------- {"id":"d80f68baee57f2cbb231f44df27ddbf22728b9649083386a44f01cdf159b398a","pubkey":"86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4","created_at":1679144385,"kind":1,"tags":[],"content":"hello world","sig":"fdb4570c73cfded2c8a29b8ca926a5a400b81246b676f5090651949806f3741c40d2df8542c70f6dbdbff4ada3123cb285fcdcb0fdf499b2bd41a97ed5f4f3bb"}
receive event:------- {"id":"c0965330b8f703cdb24916f8c74e54264ecdfebe098e79c7bec0e5586bd1ec9a","pubkey":"86ab84ff172fa8104bf9c0c22f940bc5e8bc397206a3a3566e01eccf9a05ffa4","created_at":1679143318,"kind":1,"tags":[],"content":"hello world","sig":"3d4636b45fff22a984b9cd5b5e56fec4fcc065455a2c049441f67512d1c7a6d78608dfd8a53a0fd6540ec63c3cde1d0553c8471811ac7068042116ede258754c"}

除此之外,还可以直接在 sqlite3 中查看表格看到数据。

至此,我们尝试了整个基于 Nostr 协议的完整应用。

kind 附录

kind description NIP
0 Metadata 1
1 Short Text Note 1
2 Recommend Relay 1
3 Contacts 2
4 Encrypted Direct Messages 4
5 Event Deletion 9
7 Reaction 25
8 Badge Award 58
40 Channel Creation 28
41 Channel Metadata 28
42 Channel Message 28
43 Channel Hide Message 28
44 Channel Mute User 28
1984 Reporting 56
9734 Zap Request 57
9735 Zap 57
10000 Mute List 51
10001 Pin List 51
10002 Relay List Metadata 65
22242 Client Authentication 42
24133 Nostr Connect 46
30000 Categorized People List 51
30001 Categorized Bookmark List 51
30008 Profile Badges 58
30009 Badge Definition 58
30023 Long-form Content 23
30078 Application-specific Data 78
1000-9999 Regular Events 16
10000-19999 Replaceable Events 16
20000-29999 Ephemeral Events 16
30000-39999 Parameterized Replaceable Events 33

鸣谢

本文一些内容由下面小程xu搜索提供帮助。


细节讲解并实操下: 去中心化社交协议 ---- Nostr的更多相关文章

  1. 基于NOSTR协议的“公有制”版本的Twitter,去中心化社交软件Damus用后感,一个极端走向另一个极端

    最近,一个幽灵,Web3的幽灵,在网络游荡,它叫Damus,这玩意诠释了什么叫做病毒式营销,滑稽的是,一个Web3产品却在Web2的产品链上疯狂传销,各方大佬纷纷为其背书,到底发生了什么?Damus的 ...

  2. 去中心化存储项目终极指南 | Filecoin, Storj 和 PPIO 项目技术对比(下)

    在上篇文章中,我们主要从价值定位.技术层次架构.服务质量.去中心化程度,和经济激励机制五个方面分析了三个项目的不同.在这一篇文章中,我们将着重从区块链的架构设计.数据传输技术设计和数据存储技术设计三方 ...

  3. 小众Tox——大众的“去中心化”聊天软件

    ★Tox是什么 一个反窥探的开源项目:一种基于DHT(BitTorrent)技术的即时通讯协议:一个为安全而生的加密通讯系统 .美国棱镜计划曝光后,一个名为 irungentoo 的牛人于17天后的2 ...

  4. 比特股-去中心化交易所, STEEM - 去中心化社区, EOS - 下一代智能合约

    libsnark 是实现了 zkSNARK 模式的 C++ 库.zkSNARK 是一个证明/验证计算完整性的加密方法,也即零知识验证的算法, https://github.com/scipr-lab/ ...

  5. IM 去中心化概念模型与架构设计

    今天打算写写关于 IM 去中心化涉及的架构模型变化和设计思路,去中心化的概念就是说用户的访问不是集中在一个数据中心,这里的去中心是针对数据中心而言的. 站在这个角度而言,实际上并非所有的业务都能做去中 ...

  6. 一个轻client,多语言支持,去中心化,自己主动负载,可扩展的实时数据写服务的实现方案讨论

    背景 背景是设计一个实时数据接入的模块,负责接收client的实时数据写入(如日志流,点击流),数据支持直接下沉到HBase上(兴许提供HBase上的查询),或先持久化到Kafka里.方便兴许进行一些 ...

  7. 一步步教你开发、部署第一个去中心化应用(Dapp) - 宠物商店

    今天我们来编写一个完整的去中心化(区块链)应用(Dapps), 本文可以和编写智能合约结合起来看. 写在前面 阅读本文前,你应该对以太坊.智能合约有所了解,如果你还不了解,建议你先看以太坊是什么除此之 ...

  8. ImCash:币安下架BSV之辩:规则、中立与去中心化

    一种看法是:一个引用价格数据和执行交易的加密货币交易所,其业务决策往往是在链外发生的,不受制于严格的.类似于准宪法的链上规则的约束,加密货币交易所可以拒绝任何人喜欢的价格和交易,而且这样做并不会损害底 ...

  9. 理解去中心化 稳定币 DAI

    本文转载于深入浅出区块链, 原文链接 随着摩根大通推出JPM Coin 稳定币,可以预见稳定币将成为区块链落地的一大助推器. 坦白来讲,对于一个程序员的我来讲(不懂一点专业经济和金融),理解DAI的机 ...

  10. 去中心化存储项目终极指南 | Filecoin, Storj 和 PPIO 项目异同

    Filecoin,Storj 以及 PPIO 这三个存储公链的设计思路是不一样的,没有优劣之分,写这篇文章也并不是为了争论各项目的好坏对错.去中心化存储是一个长期商业赛道,不同团队在同一个赛道上往不同 ...

随机推荐

  1. 删除没有刀路的刀具.txt

      1 UF_initialize(); 2 std::vector<tag_t>tool_tag; 3 std::vector<tag_t>del_tag; 4 tag_t ...

  2. 北大2022编译原理实践(C/C++)-sysy 编译器构建

    这是今年新推出的实践方案,由往年的sysy->IR1->IR2->RISC V变成了sysy->Koopa->RISC V,通过增量的方式让整个实践过程更容易上手 所以先 ...

  3. 使用LitJson输出格式化json文件到本地

    百度上搜了半天,竟然没有C#使用LitJson格式化输出的例子,全都是Newtonsoft.Json的,最后在litjson的官网找到了方法. 给大家分享一下: https://litjson.net ...

  4. 开始 go

    为什么开始想转 go ? 毕业三年多,一直从事的是 Java , 大学学的也是 Java ,本来想一直干下去的,可是似乎 Java 水涨船高,面试要求也越来越高. 曾经一起毕业的同事自学 go ,已经 ...

  5. MySql 错误:建表时出错1067 - Invalid default value for 'id'

    问题描述: 建表时报错无效的默认值: CREATE TABLE `product`( `id` INT(10) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT ...

  6. curl 查看响应时间

    curl -o /dev/null -s -w "time_namelookup:%{time_namelookup}\ntime_connect: %{time_connect}\ntim ...

  7. PR2022(Premiere Pro 2022)Mac/win最新中文版

    Adobe Premiere Pro 2022 Mac/win是用于专业级别的视频编辑软件,一个基于时间轴的视频处理工具,具有许多用于生成高端视频的不同功能.Adobe Premiere Pro最重要 ...

  8. 07 HBase操作

    1.理解HBase表模型及四维坐标:行键.列族.列限定符和时间戳. 2.启动HDFS,启动HBase,进入HBaseShell命令行. 3.列出HBase中所有的表信息list 4.创建表create ...

  9. html:表格

    HTML:表格1.表格三标签:(1)<table> 表格(2)<tr>  行(3)<td>  单元格 2.表格的基本语法和结构<table> <t ...

  10. MySql5.7基础配置

    MySql5.7基础配置 [client] #设置mysql客户端的字符集 default-character-set=utf8 [mysqld] #设置mysql端口为3306 port = 330 ...