Python-区块链开发实用指南(五)

317 阅读1小时+

Python 区块链开发实用指南(五)

原文:zh.annas-archive.org/md5/E6FBF7D7A6EED49747FB2B635A55F938

译者:飞龙

协议:CC BY-NC-SA 4.0

第十一章:使用 ipfsapi 与 IPFS 进行交互

在本章中,我们将学习如何使用 Python 以编程方式与 IPFS 进行交互。我们可以在这里进行一些交互,例如添加文件,检索文件,托管可变文件,订阅主题,发布主题,并将文件复制到可变文件系统MFS)。首先,我们必须安装 IPFS 软件并启动它。然后,我们将学习如何安装 IPFS Python 库,并了解其大部分 API。

在本章中,我们将涵盖以下主题:

  • 安装 IPFS 软件及其库

  • 内容哈希

  • ipfsapi API

安装 IPFS 软件及其库

在撰写本文时,只有两种 IPFS 实现:go-ipfs(用 Go 语言编写)和js-ipfs(用 JavaScript 编写)。截至目前,还没有用 Python 编写的 IPFS 实现。Go 实现是更受欢迎的,因此我们将使用它。

转到dist.ipfs.io/#go-ipfs,并为您的平台下载软件。对于 Ubuntu Linux,文件名为go-ipfs_v0.4.18_linux-amd64.tar.gz

使用以下命令行提取此内容:

$ tar xvfz go-ipfs_v0.4.18_linux-amd64.tar.gz

然后,使用以下命令安装二进制文件:

$ cd go-ipfs
$ sudo ./install.sh

此步骤是可选的。在这里,我们将IPFS_PATH环境变量导出到我们的 shell:

$ export IPFS_PATH=/path/to/ipfsrepo

这是ipfs存储文件的位置。您可以将此语句存储在~/.bashrc中。默认情况下(没有此环境变量),ipfs将使用~/.ipfs(主目录中的.ipfs目录)作为存储数据的位置。

设置环境变量后,初始化ipfs本地存储库。您只需执行此步骤一次:

$ ipfs init

如果您在云中运行ipfs(例如 Amazon Web Services,Google Cloud Platform,Digital Ocean 或 Azure),您应该使用服务器配置文件标志:

$ ipfs init --profile server

否则,您将收到来自云提供商的烦人警告信,因为 IPFS 守护程序默认情况下(没有服务器配置文件标志),会执行类似于端口扫描的操作。

然后,启动守护程序,如下所示:

$ ipfs daemon

默认情况下,API 服务器正在端口 5001 上监听。我们将通过此端口以编程方式与 IPFS 进行交互。默认情况下,它只在本地侦听。如果您想向外界打开此端口,请小心。IPFS 中没有访问控制列表ACL)。任何可以访问此端口的人都可以向 IPFS 上传数据。

默认情况下,网关服务器正在端口 8080 上监听。我们使用此端口从 IPFS 点对点文件系统下载文件。默认情况下,Swarm 正在端口 4001 上监听。这是其他节点从我们的存储中下载文件的方式。所有这些端口都可以更改。

IPFS 有一个仪表板,可以通过以下链接访问:localhost:5001/webui。以下是仪表板的屏幕截图:

正如您所看到的,大多数 IPFS 节点位于美国,中国和德国。

单击 Peers 选项卡,以查看 IPFS 节点根据其 IP 地址的分布,如下屏幕截图所示:

可以在此选项卡中看到节点,包括它们的 IP 地址。如果您担心节点的隐私,请记住隐私功能的开发仍处于初期阶段。

您可以在设置选项卡中配置 IPFS 设置,如下屏幕截图所示:

现在我们的 IPFS 守护程序已启动,让我们安装我们的ipfs Python 库。

打开一个新的终端,因为我们不想打扰我们的守护程序。然后,运行以下命令:

$ virtualenv -p python3.6 ipfs-venv
$ source ipfs-venv/bin/activate
(ipfs-venv) $ pip install ipfsapi

以前,ipfs Python 库被称为py-ipfs-api,但现在已更名为ipfsapi

内容哈希

在 IPFS 快速入门文档中(docs.ipfs.io/introduction/usage),他们教您的第一件事是下载可爱的猫图片。使用以下代码来执行此操作:

$ ipfs cat /ipfs/QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ/cat.jpg >cat.jpg
$ eog cat.jpg 

运行上述代码后,将下载猫图片,并且您将得到以下输出:

eog是 Ubuntu 中的图像查看器。

为了遵循传统,让我们创建一个 Python 脚本,以便使用 Python 以编程方式下载前面的图像,并将脚本命名为download_cute_cat_picture.py

import ipfsapi

c = ipfsapi.connect()
cute_cat_picture = 'QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ/cat.jpg'
c.get(cute_cat_picture)

执行此脚本后,图像将在您的目录中命名为cat.jpg

正如您可能已经注意到的那样,在长哈希之后有一个cat.jpg文件名。从技术上讲,我们在这里做的是在包含一张可爱猫图片的目录中下载文件。如果您愿意,可以尝试一下。要这样做,创建另一个脚本并将其命名为download_a_directory_of_cute_cat_picture.py,然后运行以下代码:

import ipfsapi

c = ipfsapi.connect()
directory = 'QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ'
c.get(directory)

执行此脚本后,您将在包含此脚本的目录中得到一个名为QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ的目录。如果您查看此目录的内部,将找到猫图片文件。

让我们逐行查看脚本,以了解ipfsapi库的用法。您可以使用以下代码导入库:

import ipfsapi

以下代码用于获取到 IPFS 守护程序的连接对象:

c = ipfsapi.connect()

connect方法接受一些参数。最重要的两个参数是hostport

c = ipfsapi.connect(host="ipfshost.net", port=5001)

默认情况下,我们连接到本地主机的 5001 端口:

c.get(cute_cat_picture)

然后,我们使用 IPFS HTTP API 的方法从c对象中。get是用于与 IPFS 守护程序交互的方法之一。对于此方法,通常有一个与ipfs客户端软件相对应的参数:

$ ipfs get QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ/cat.jpg

您可能会注意到,在创建第一个 Python 脚本之前,我们在命令行界面中使用了ipfs cat命令。但是,在脚本中,我们使用了get方法。ipfsapi库中也有一个cat方法。

get方法用于下载文件,而cat方法用于获取文件的内容。

让我们创建一个使用cat方法的脚本,并将其命名为cat_cute_cat.py

import ipfsapi

c = ipfsapi.connect()
result = c.cat('QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ/cat.jpg')
with open('cat.jpg', 'wb') as f:
    f.write(result)

cat方法返回文件内容的字节对象。它接受两个可选参数,offsetlengthoffset是文件中要开始获取内容的起始位置。length是从offset位置开始获取的内容的长度。如果要构建一个具有暂停和恢复功能的下载管理器(或视频流播放器),这些参数非常重要。您可能并不总是想要下载整个文件。

现在,让我们将一个文件添加到 IPFS。要做到这一点,创建一个简单的文件并将其命名为hello.txt。这是文件的内容:

I am a good unicorn.

确保在字符串之后有一个新行:

$ cat hello.txt
I am a good unicorn.
$

如果命令提示符出现在字符串的下一行,则一切正常。您可以继续进行。

但是,假设命令提示符出现在字符串的右侧,如下面的代码块所示:

$ cat hello.txt
I am a good unicorn.$

这意味着您没有新行,需要在字符串之后添加它。

现在,让我们创建一个脚本将此文件添加到 IPFS,并将其命名为add_file.py

import ipfsapi

c = ipfsapi.connect()
result = c.add('hello.txt')
print(result)

执行此代码将给出以下输出:

(ipfs-venv) $ python add_file.py
{'Name': 'hello.txt', 'Hash': 'QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR', 'Size': '29'}

我们可以使用catget方法检索文件的内容,即I am a good unicorn.\n。让我们在名为get_unicorn.py的脚本中使用cat方法,如下面的代码块所示:

import ipfsapi

c = ipfsapi.connect()
result = c.cat('QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR')
print(result)

运行此代码将给出以下输出:

(ipfs-venv) $ python get_unicorn.py
b'I am a good unicorn.\n'

正如我们在第十章中提到的星际文件系统-一个勇敢的新文件系统,我们通过哈希获取文件的内容。通过这种方式,我们只检索内容,而不是文件的名称。

但是,如何将b'I am a good unicorn.\n'转换为'QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR'?只是通过对内容进行哈希吗?例如,要对文件的内容进行哈希,你可以使用 SHA-256 哈希函数:

import hashlib
the_hash = hashlib.sha256(b'I am a good unicorn.\n').hexdigest()

不要那么快!原来你需要先了解 protobuf、multihash 和 base58 的过程。让我们在接下来的部分讨论这些。

Protobuf

如果你尝试安装 Google 开源软件,比如Tensorflow,你会遇到 protobuf,因为它被Tensorflow使用。Protobuf是一个序列化库。如果你从官方文档中学习 Python,你会知道 Pickle 是一种序列化数据的方式。如果你学习 Web 开发编程,很可能你会使用.json.xml作为数据序列化。

在将b'I am a good unicorn.\n'传递给 IPFS 之前,我们需要将我们的数据包装在一个数据结构中。让我们创建一个脚本来证明我的说法,并将脚本命名为get_unicorn_block.py

import ipfsapi

c = ipfsapi.connect()
result = c.block_get('QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR')
print(result)

运行脚本将允许你看到文件内容被其他内容包裹:

(ipfs-venv) $ python get_unicorn_block.py
b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15'

我们可以看到我们的文件内容仍然完整,b'I am a good unicorn.\n',在神秘的字符串之间。左边和右边的垃圾字符是什么?这是 IPFS 中数据节点的数据结构。

在我们对这个序列化数据进行反序列化之前,让我们快速学习如何使用protobuf

  1. 使用以下命令安装protobuf-compiler
$ sudo apt-get install protobuf-compiler

你的protobuf编译器是protoc

$ protoc --version
libprotoc 2.6.1
  1. 然后,使用以下命令安装 Python 的protobuf库:
(ipfs-venv) $ pip install protobuf
  1. 在使用protobuf对数据进行序列化之前,你需要先创建一个数据结构格式。这个格式必须保存在一个文件中。让我们把格式文件命名为crypto.proto,并使用以下脚本:
syntax = "proto2";

package crypto;

message CryptoCurrency {
 required string name = 1;
 optional int32 total_supply = 2;

 enum CryptoType {
 BITCOIN = 0;
 ERC20 = 1;
 PRIVATE = 2;
 } required CryptoType type = 3 [default = ERC20];
}

当你查看这个数据结构时,它就像一个没有方法的结构或类。在声明你使用的语法之后,你声明package以避免名称冲突。message就像另一种主流编程语言中的类或结构关键字。这个message是许多数据类型的封装。在我们的情况下,它们是stringint32enum

  1. 在 Python 中对数据进行 protobuf 序列化之前,我们需要将这个.proto文件转换成一个 Python 模块文件:
$ protoc crypto.proto --python_out=.

python_out参数用于指示你想要将这个 Python 文件输出到哪个目录。

你应该为你生成的crypto_pb2.py文件。参考 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/tree/master/chapter_11

如果你没有阅读文件顶部的注释,让我为你读一下:不要直接编辑这个文件。如果你想要在序列化过程中更改数据结构,你需要修改.proto文件,然后编译它。现在你已经为你生成了这个Python库文件,你可以丢掉.proto文件。但是,保留它作为文档是个好主意。

现在,让我们使用一个 Python 脚本测试序列化和反序列化过程。将脚本命名为serialize_crypto_data.py

import crypto_pb2

cryptocurrency = crypto_pb2.CryptoCurrency()
cryptocurrency.name = 'Bitcoin Cash'
cryptocurrency.total_supply = 21000000
cryptocurrency.type = crypto_pb2.CryptoCurrency.BITCOIN

serialized_data = cryptocurrency.SerializeToString()
print(serialized_data)

cryptocurrency2 = crypto_pb2.CryptoCurrency()
cryptocurrency2.ParseFromString(serialized_data)
print(cryptocurrency2)

如果你执行这个脚本,你将得到以下输出:

(ipfs-venv) $ python serialize_crypto_data.py
b'\n\x0cBitcoin Cash\x10\xc0\xde\x81\n\x18\x00'
name: "Bitcoin Cash"
total_supply: 21000000
type: BITCOIN

序列化输出,b'\n\x0cBitcoin Cash\x10\xc0\xde\x81\n\x18\x00',类似于我们在 IPFS 中的独角兽数据块。如果你解析这个二进制数据,你应该得到原始的 Python 对象。

现在你了解了 protobuf 的基本用法,让我们回到 IPFS 中的块数据:

(ipfs-venv) $ python get_unicorn_block.py
b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15'

这是 protobuf 中的序列化数据。在我们对其进行反序列化之前,我们需要获取相关的.proto文件。显然,我们需要两个.proto文件,unixfs.protomerkledag.proto

unixfs.proto可以从github.com/ipfs/go-unixfs/blob/master/pb/unixfs.proto下载,而merkeldag.proto可以从github.com/ipfs/go-merkledag/blob/master/pb/merkledag.proto下载。

以下代码块是unixfs.proto文件的内容:

syntax = "proto2";

package unixfs.pb;

message Data {
    enum DataType {
        Raw = 0;
        Directory = 1;
        File = 2;
        Metadata = 3;
        Symlink = 4;
        HAMTShard = 5;
    }

    required DataType Type = 1;
    optional bytes Data = 2;
    optional uint64 filesize = 3;
    repeated uint64 blocksizes = 4;

    optional uint64 hashType = 5;
    optional uint64 fanout = 6;
}

message Metadata {
    optional string MimeType = 1;
}

以下代码块是merkledag.proto文件的内容:

package merkledag.pb;

import "code.google.com/p/gogoprotobuf/gogoproto/gogo.proto";

option (gogoproto.gostring_all) = true;
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
option (gogoproto.goproto_stringer_all) = false;
option (gogoproto.stringer_all) =  true;
option (gogoproto.populate_all) = true;
option (gogoproto.testgen_all) = true;
option (gogoproto.benchgen_all) = true;
option (gogoproto.marshaler_all) = true;
option (gogoproto.sizer_all) = true;
option (gogoproto.unmarshaler_all) = true;

...
...

// An IPFS MerkleDAG Node
message PBNode {

  // refs to other objects
  repeated PBLink Links = 2;

  // opaque user data
  optional bytes Data = 1;
}

为了简化流程,你应该删除merkledag.proto文件中的以下行:

import "code.google.com/p/gogoprotobuf/gogoproto/gogo.proto";

option (gogoproto.gostring_all) = true;
option (gogoproto.equal_all) = true;
option (gogoproto.verbose_equal_all) = true;
option (gogoproto.goproto_stringer_all) = false;
option (gogoproto.stringer_all) =  true;
option (gogoproto.populate_all) = true;
option (gogoproto.testgen_all) = true;
option (gogoproto.benchgen_all) = true;
option (gogoproto.marshaler_all) = true;
option (gogoproto.sizer_all) = true;
option (gogoproto.unmarshaler_all) = true

然后,使用以下命令编译两个.proto文件:

$ protoc unixfs.proto merkledag.proto --python_out=.

完成后,你将得到生成的unixfs_pb2.pymerkledag_pb2.py文件。

让我们创建一个脚本来解码我们的块数据,b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15',并将脚本命名为unserialize_unicorn.py

import unixfs_pb2
import merkledag_pb2

outer_node = merkledag_pb2.PBNode()
outer_node.ParseFromString(b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15')
print(outer_node)

unicorn = unixfs_pb2.Data()
unicorn.ParseFromString(outer_node.Data)
print(unicorn)

运行脚本。这将给出以下输出:

(ipfs-venv) $ python unserialize_unicorn.py
Data: "\010\002\022\025I am a good unicorn.\n\030\025"

Type: File
Data: "I am a good unicorn.\n"
filesize: 21

让我们来剖析一下这里发生了什么。我们的原始数据,b'I am a good unicorn.\n',被包裹在unixfs proto 模块的Data中,然后再次被包裹在merkledag proto 模块的PBNode中。这就是为什么我们首先在脚本中用PBNode反序列化序列化的数据。然后,我们用Data反序列化结果。

Multihash

现在,让我们对序列化数据进行哈希。IPFS 使用 multihash 对数据进行哈希。这意味着它不仅输出哈希输出,还输出它使用的哈希函数、来自该哈希函数的哈希输出的长度以及该哈希函数的哈希输出。

让我们看一个 multihash 的使用示例。假设我们要哈希的数据是b'i love you'。我们选择sha256作为哈希函数,如下:

>>> from hashlib import sha256
>>> sha256(b'i love you').hexdigest()
'1c5863cd55b5a4413fd59f054af57ba3c75c0698b3851d70f99b8de2d5c7338f

让我们来检查一下这个哈希输出的长度:

>>> len('1c5863cd55b5a4413fd59f054af57ba3c75c0698b3851d70f99b8de2d5c7338f')
64

由于十六进制格式的数字始终占据两个字符,哈希输出的长度为 32(64/2)。但是,我们想要 32 的十六进制版本,即 0x20 或20

有一个哈希函数表,列出了 multihash 支持的所有哈希函数(sha1、shake、blake、keccak 等)。可以在这里看到:github.com/multiformats/multicodec/blob/master/table.csv

正如你所看到的,sha256被赋予了数字12

现在,我们使用以下命令将它们组合起来:

Hash function + the length of hash output from hash function + hash output from hash function
12 + 20 + 1c5863cd55b5a4413fd59f054af57ba3c75c0698b3851d70f99b8de2d5c7338f

或者,我们可以使用以下命令:

12201c5863cd55b5a4413fd59f054af57ba3c75c0698b3851d70f99b8de2d5c7338f

让我们再做一次,但是使用另一个函数,即sha1

>>> from hashlib import sha1
>>> sha1(b'i love you').hexdigest()
'bb7b1901d99e8b26bb91d2debdb7d7f24b3158cf'
>>> len('bb7b1901d99e8b26bb91d2debdb7d7f24b3158cf')
40

20 的十六进制版本是 0x14,或14

sha1哈希函数被赋予了数字 0x11 或11,来自哈希函数表。因此,输出如下:

11 + 14 + bb7b1901d99e8b26bb91d2debdb7d7f24b3158cf
1114bb7b1901d99e8b26bb91d2debdb7d7f24b3158cf

那么,为什么要使用 multihash 而不是普通的哈希函数,比如sha1sha256keccak256?有时的论点是哈希函数已经被破解,这意味着有人可以在合理的时间内找到两个不同的输入,得到相同的哈希输出。如果发生这种情况,那就非常危险。哈希用于完整性检查。想象一下,我给你发送了一份秘密文件,用来治愈癌症。为了确保它没有被篡改,我们对这份文件进行哈希,然后广播哈希输出。因此,任何想要了解这份文件的人都需要在阅读和执行之前验证文件的哈希。然而,想象一下我的敌人可以创建一个不同的文件。现在,这份文件不再是治愈癌症的文件,而是创建病毒的指南,但它仍然具有相同的哈希输出。如果你对错误的文件进行哈希,你将无意中执行该文件并创建病毒。

如果一个哈希函数被破解了(而且已经发生了,sha1哈希函数已经被破解了),程序员们需要升级他们的系统。然而,他们会遇到困难,因为通常他们对哈希函数做了一些假设。例如,如果他们使用sha1函数,他们会期望从哈希函数得到的输出是 20 个数字的长度。如果他们选择将哈希函数升级到sha256,他们需要替换所有预期旧哈希函数处理时长度为 20 个字符的代码,这是很麻烦的。

使用multihash,我们期望升级过程会变得简化,因为哈希函数的输出的函数和长度都嵌入在multihash函数的输出中。我们不再对哈希输出的长度做假设。

如果你仍然不能理解multihash的动机,让我们用以下代码来实验一下:

(ipfs-venv) $ pip install pymultihash
(ipfs-venv) $ python
>>> import multihash
>>> the_universal_hash = multihash.digest(b'i love you', 'sha1')
>>> the_universal_hash.verify(b'i love you')
True

你有没有注意到,当我们想要检查b'i love you'数据的完整性时,我们不会对哈希输出的长度做假设?然后我们发现了一个坏消息,那就是sha1哈希函数已经被破解了。为了升级我们的系统,我们需要做的只是将'sha1'字符串简单地替换为'sha2_256'字符串:

>>> the_universal_hash = multihash.digest(b'i love you', 'sha2_256')
>>> the_universal_hash.verify(b'i love you')
True

通过使用 multihash,升级 IPFS 软件中的哈希函数变得非常容易。哈希函数只是一个配置问题。

Base58

我们需要学习的最后一件事是base58。Base58 是base64的修改版本。这通常用于将二进制数据编码为 ASCII 字符串。以下代码块用于将b'i love you'编码为 ASCII 字符串:

>>> import base64
>>> base64.b64encode(b'i love you')
b'aSBsb3ZlIHlvdQ=='

base64模块是 Python 标准库的一部分。

通常,你不会用base64来编码另一个 ASCII 字符串。相反,你会编码二进制数据,比如一个图片文件。如果你用文本编辑器打开cat.jpg,你会得到类似于以下截图中显示的无意义文本:

这是使用base64进行编码的一个完美例子。为什么你想要用base64来编码二进制数据呢?一个用例是,如果你想在电子邮件中给你的朋友附上一张可爱的猫图片。电子邮件协议不允许二进制数据。以下代码块展示了如果我们附上图片会得到什么结果:

>>> c = None
>>> with open('cat.jpg', 'rb') as f:
...     c = f.read()
... 
>>> import base64
>>> base64.b64encode(c)
b'/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAMgBQADASIAAhEBAxEB/8QAHwAAAAYDAQEBAAAAAAAAAAAAAAQFBgcIAgMJCgEL/8QAbhAAAAQCBgQJCAYHBQQGBQIXAgMEBQEGAAcREhMhFCMxQQgiJDNRYXGB8BUyNEORobHBQkRT0eHxCRYlUlRjczViZHSDF3KEkyZFgpSjszZVZZKkw9MKGHWitMRWhdTj5Eay8xkndoaVlsLS4v/EABwBAAEFAQEBAAAAAAAAAAAAAAACAwQFBgcBCP/EAEwRAAAEBAIIBAUDAgQFAgUCBwECAxEABCExQVEFEhNhcYGR8KGxwdEGFCIj4TIz8SRDBxVCUzREUmJjFnNUco……………...s0fQyVCRRbpSWOyyylf5pKJGnOLTlixICLAcYL6fZ25/hSCW3hIrDGFOwjWKFEvluXlXQ8MosvyoeVg4umYFuP8AV4psrOiyiO8V8M5xBw1BwcG8WAJeNhmW5/du25QypGNKzQKJcAEXxthvCtWiuBRPVVA9twcApcfLHKP/2Q=='

使用base64进行编码的过程(如何将b'i love you'转换为b'aSBsb3ZlIHlvdQ==')超出了本书的范围。如果你很好奇,你可以查看base64规范。

现在你已经熟悉了base64base58会变得非常简单。在base58编码中,打印时会产生歧义的字母,如 0、O、I 和 l,都被移除了。+(加号)和/(斜杠)字符也被移除了。这种base58编码是由中本聪设计的,用于编码大整数。比特币地址本质上就是一个非常大的整数。如果你曾经转移过任何加密货币的金额(不一定是比特币),你很可能会多次检查地址以确保地址是正确的。例如,你想要向你奶奶的比特币地址转移 1 比特币,她的地址是1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2。如果你和大多数人一样,你会多次验证地址的正确性,以确保地址不是错误的。通过移除 0、O、I 和 l 等模棱两可的字符,你会发现更容易确保这个地址是它应该是的。Base58 是软件中用来解决这个问题的良好用户体验设计之一。

因此,base58并不是设计用来编码可爱的猫图片的。你应该使用base64编码来实现这个目的。

让我们安装base58库来进行实验:

>>> import base58
>>> base58.b58encode(b'i love you')
b'6uZUjTpoUEryQ8'

通过使用base58,我们可以创建一个长的十六进制字符串,可以轻松地用我们自己的眼睛进行检查和验证。

结合 protobuf、multihash 和 base58

现在您已经了解了 protobuf、multihash 和 base58,我们终于可以理解b'I am a good unicorn.\n'文件内容如何变成'QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR'的谜题了。

b'I am a good unicorn.\n'数据被包装在 IPFS 节点中,并使用 protobuf 序列化为b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15'。以下是如何在 Python 中执行的。

创建一个名为serialize_unicorn.py的脚本:

import unixfs_pb2
import merkledag_pb2

precious_data = b'I am a good unicorn.\n'

unicorn = unixfs_pb2.Data()
unicorn.Type = unixfs_pb2.Data.File
unicorn.Data = precious_data
unicorn.filesize = len(precious_data)

serialized_unicorn_node = unicorn.SerializeToString()

outer_node = merkledag_pb2.PBNode()
outer_node.Data = serialized_unicorn_node
print(outer_node.SerializeToString())

运行它。您应该得到以下输出:

(ipfs-venv) $ python serialize_unicorn.py
b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15'

然后,这个 protobuf 序列化的数据被sha256(IPFS 中的 multihash 默认使用sha256哈希函数)哈希为'912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536'

以下是如何在 Python 中执行的:

>>> import hashlib
>>> hashlib.sha256(b'\n\x1b\x08\x02\x12\x15I am a good unicorn.\n\x18\x15').hexdigest()
'912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536'

IPFS 在 multihash 表中使用的sha256函数数量为 12。表可以在这里看到:github.com/multiformats/multicodec/blob/master/table.csv

哈希输出的长度为32,或者用十六进制表示为 0x20。一个十六进制数字占据两个字符:

>>> len('912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536') // 2
32
>>> hex(32)
'0x20'

让我们将它们连接起来:

12 + 20 + 912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536
1220912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536

如果您使用 base58 编码对此输出进行编码,您应该得到'QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR'

以下是如何在 Python 中执行的。b58encode()方法只接受字节对象,而不是十六进制对象,因此您必须首先将十六进制字符串转换为字节对象:

>>> import codecs
>>> codecs.decode('1220912d1af8f0013cd12a514859d20e9a196eb2845981408a84cf3543bb359a4536', 'hex')
b'\x12 \x91-\x1a\xf8\xf0\x01<\xd1*QHY\xd2\x0e\x9a\x19n\xb2\x84Y\x81@\x8a\x84\xcf5C\xbb5\x9aE6'

codecs是 Python 标准库的一部分。

执行代码后,您将得到以下输出:

>>> base58.b58encode(b'\x12 \x91-\x1a\xf8\xf0\x01<\xd1*QHY\xd2\x0e\x9a\x19n\xb2\x84Y\x81@\x8a\x84\xcf5C\xbb5\x9aE6')
b'QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR'

大功告成!谜题终于解开了。

ipfsapi API

让我们回到 ipfsapi 的 API。我们已经使用 IPFS API 添加了一个文件,并收到了用于引用文件内容的哈希。但是,如果我们添加一个大文件,它将被分成许多块。这是为了提高效率。

让我们从 Unsplash 下载一个相当大的图像文件。转到unsplash.com/photos/UBtUB4Qc-_4下载一个图像文件。下载的文件名为milada-vigerova-1284157-unsplash.jpg。将其放在与您的 IPFS Python 脚本文件相同的目录中。您可以使用任何图像文件,但请确保其大小至少为 1 MB。但是,如果您使用另一个图像文件,您应该得到一个不同的哈希。

使用以下代码块创建一个名为add_image_file.py的脚本:

import ipfsapi

c = ipfsapi.connect()
result = c.add('dose-juice-1184429-unsplash.jpg')
print(result)

运行它。您应该得到以下输出:

(ipfs-venv) $ python add_image_file.py
{'Name': 'milada-vigerova-1284157-unsplash.jpg', 'Hash': 'QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM', 'Size': '2604826'}

接下来,创建另一个脚本来列出此块中的所有块,并将脚本命名为list_blocks.py

import ipfsapi
import pprint

c = ipfsapi.connect()
blocks = c.ls('QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM')
pp = pprint.PrettyPrinter(indent=2)
pp.pprint(blocks)

pprint是 Python 标准库的一部分。

运行脚本。您应该得到以下输出:

(ipfs-venv) $ python list_blocks.py
{ 'Objects': [ { 'Hash': 'QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM',
 'Links': [ { 'Hash': 'Qmahxa3MABVtHWh7b2cbQb9hEfiuvwKeYceaqrW8pZjemV',
 'Name': '',
 'Size': 262158,
 'Type': 2},
 { 'Hash': ...
... 'QmbSa1vj3c1edyKFdTCaT88pYGTLS9n2mpRuL2B2NLUygv',
 'Name': '',
 'Size': 244915,
 'Type': 2}]}]}

正如我在第十章中解释的那样,星际文件系统-一个崭新的文件系统,一个大文件不会立即被哈希,因为内存问题。相反,它将被分成许多块。每个块的大小为 262,158 字节,最后一个除外。但是,您可以配置块的大小。每个块将被单独哈希。然后,文件内容的根哈希是这些哈希的组合。IPFS 使用默克尔树来计算根哈希。当然,您必须在使用 protobuf 序列化之前将每个块包装在 IPFS 节点内。然后,将包含所有这些块链接的容器节点。

您可以在没有.proto文件的情况下对以下 IPFS 块进行逆向工程:

{'Name': 'milada-vigerova-1284157-unsplash.jpg', 'Hash': 'QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM', 'Size': '2604826'}

记住这个图像文件的哈希。获取此文件内容的 IPFS 块。您可以使用 Python 脚本或 IPFS 命令行实用程序来执行:

$ ipfs block get QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM > block.raw

我们将以二进制格式保存块,然后可以使用protoc编译器解码此二进制文件。

$ protoc --decode_raw < block.raw

您应该得到以下结果:

2 {
  1 {
    2: "\267\301\242\262\250qw\216+\237\301\273\'\360%\"\2022\201#R\364h\262$\357\227\2355\244>x"
  }
  2: ""
  3: 262158
}
...
...
1 {
  1: 2
  3: 2604197
  4: 262144
  4: 262144
...
...
  4: 262144
  4: 244901
}

你可能对这种结构很熟悉。当你在没有 proto 文件的情况下解码 protobuf 中的序列化数据时,问题在于你必须猜测某个块内的 1、2、3 和 4 代表什么。如果你有 proto 文件,这一行3: 2604197将变成filesize: 2604197。因此,在解码 protobuf 中的序列化数据之前获取 proto 文件是一个好主意。

我们可以从这些块中重建原始文件。让我们创建脚本并将其命名为construct_image_from_blocks.py

import ipfsapi

c = ipfsapi.connect()

images_bytes = []

blocks = c.ls('QmV5KPoHHqbq2NsALniERnaYjCJPi3UxLnpwdTkV1EbNZM')
for block in blocks['Objects'][0]['Links']:
    bytes = c.cat(block['Hash'])
    images_bytes.append(bytes)

images = b''.join(images_bytes)
with open('image_from_blocks.jpg', 'wb') as f:
    f.write(images)

运行脚本后,如果你打开image_from_blocks.jpg,你将看到原始的图像文件。

我们已经添加了一个文件。现在,让我们尝试添加一个文件目录。

创建一个名为mysite的目录。在这个目录里,创建一个名为img的目录。将cat.jpg图像文件放在这个img目录中。然后,在img目录旁边创建一个名为index.html的文件。

index.html的内容如下代码块所示:

<html>
  <head>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
  </head>
  <body>
    <img src="img/cat.jpg" class="rounded-circle" />
  </body>
</html>

然后,在img目录旁边创建一个README.md文件。下面的代码块是README.md文件的内容:

This is Readme file.

现在,创建一个 Python 脚本将这个目录添加到 IPFS,并将脚本命名为add_directory.py

import ipfsapi
import pprint

c = ipfsapi.connect()
result = c.add('mysite', True)

pp = pprint.PrettyPrinter(indent=2)
pp.pprint(result)

运行脚本将给你以下输出:

(ipfs-venv) $ python add_directory.py
[ { 'Hash': 'QmWhZDjrm1ncLLRZ421towkyYescK3SUZdWEM5GxApfxJe',
 'Name': 'mysite/README.md',
 'Size': '29'},
 { 'Hash': 'QmUni2ApnGhZ89JEbmPZQ1QU9wcinnCoujjrYAy9TCQQjj',
 'Name': 'mysite/index.html',
 'Size': '333'},
 { 'Hash': 'Qmd286K6pohQcTKYqnS1YhWrCiS4gz7Xi34sdwMe9USZ7u',
 'Name': 'mysite/img/cat.jpg',
 'Size': '443362'},
 { 'Hash': 'QmW2WQi7j6c7UgJTarActp7tDNikE4B2qXtFCfLPdsgaTQ',
 'Name': 'mysite/img',
 'Size': '443417'},
 { 'Hash': 'QmZamPcNnfZjjTkoyrYjYMEA8pp29KmpmkuSvkicSGiZDp',
 'Name': 'mysite',
 'Size': '443934'}]

add方法的第二个参数涉及recursive参数。IPFS 使用 Merkle DAG 数据结构来保存这个文件目录。

我们可以在浏览器中打开我们的网站,使用以下 URL:http://localhost:8080/ipfs/QmZamPcNnfZjjTkoyrYjYMEA8pp29KmpmkuSvkicSGiZDp/。以下截图是网站在浏览器中的显示方式:

你也可以使用以下 URL 从另一个网关(使用另一个节点)访问 IPFS 路径:ipfs.io/ipfs/QmZamPcNnfZjjTkoyrYjYMEA8pp29KmpmkuSvkicSGiZDp/。根据你的互联网连接,这可能需要一些时间,因为 ipfs.io 服务器中的一个节点需要定位你计算机中的内容。

IPNS

能够发布由哈希保护完整性的文件或文件目录是很棒的。然而,偶尔你可能希望能够发布一个具有相同链接的动态文件。我在这里的意思是,哈希链接会在不同的时间生成不同的内容。一个用例是你想发布新闻。根据情况,新闻可能每分钟或每小时都会改变。

你可以通过使用星际命名系统IPNS)来做到这一点。哈希链接是从我们的 IPFS 节点中的加密密钥派生出来的。当我们启动 IPFS 守护程序时,我们成为 IPFS 点对点网络中的众多节点之一。我们的身份是基于一个加密密钥的。

让我们创建两个星座预测。这里的预测应该随着时间的推移而改变。第一个文件名是horoscope1.txt,这个文件的内容在下面的代码块中给出:

You will meet the love of your life today!

第二个文件名是horoscope2.txt,这个文件的内容在下面的代码块中给出:

You need to be careful when going outside!

让我们使用这个 Python 脚本添加这两个文件,命名为add_horoscope_predictions.py

import ipfsapi

c = ipfsapi.connect()
result = c.add('horoscope1.txt')
print(result)
result = c.add('horoscope2.txt')
print(result)

运行这将给你以下输出:

(ipfs-venv) $ python add_horoscope_predictions.py
{'Name': 'horoscope1.txt', 'Hash': 'QmTG4eE6ruUDhSKxqwofJXXqDFAmNzQiGdo4Z7WvVdLZuS', 'Size': '51'}
{'Name': 'horoscope2.txt', 'Hash': 'Qme1FUeEhA1myqQ8C1sCSXo4dDJzZApGD6StE26S72ZqyU', 'Size': '51'}

注意我们在输出中获得的这两个哈希值。

然后,创建一个脚本来列出我们所有的密钥,并将脚本命名为keys_list.py

import ipfsapi

c = ipfsapi.connect()
print(c.key_list())

运行上述脚本将给你以下输出:

(ipfs-venv) $ python keys_list.py
{'Keys': [{'Name': 'self', 'Id': 'QmVPUMd7mFG54zKDNNzPRgENsr5VTbBxWJThfVd6j9V4U8'}]}

现在,让我们发布我们的第一个星座预测。使用以下代码块创建一个名为publish_horoscope1.py的 Python 脚本:

import ipfsapi

c = ipfsapi.connect()
peer_id = c.key_list()['Keys'][0]['Id']
c.name_publish('QmY7MiYeySnsed1Z3KxqDVYuM8pfiT5gGTqprNaNhUpZgR')
result = ipfs.cat('/ipns/' + peer_id)
print(result)

运行这可能需要一些时间。在 IPNS 中发布文件有点慢。如果你足够耐心,你会得到以下输出:

(ipfs-venv) $ python publish_horoscope1.py
b'You will meet the love of your life today!\n'

你可以使用name_publish()方法发布内容。它接受内容的哈希链接(IPFS 路径,而不是文件名)作为第一个参数。

然后,要从 IPFS 访问内容,可以使用catget方法。在这里,我们使用cat方法。cat方法的参数不是哈希链接或 IPFS 路径,而是 IPNS 路径,它只是一个可以从keys_list.py脚本中获取的密钥。您必须在此之前加上'/ipns/'字符串。因此,IPNS 路径是'/ipns/ QmVPUMd7mFG54zKDNNzPRgENsr5VTbBxWJThfVd6j9V4U8'

现在,让我们发布更多数据。使用以下代码块创建一个名为publish_horoscope2.py的脚本:

import ipfsapi

c = ipfsapi.connect()
peer_id = c.key_list()['Keys'][0]['Id']
c.name_publish('Qme1FUeEhA1myqQ8C1sCSXo4dDJzZApGD6StE26S72ZqyU')
result = ipfs.cat('/ipns/' + peer_id)
print(result)

运行此脚本将给出与上一个不同的结果:

(ipfs-venv) $ python publish_horoscope2.py
b'You need to be careful when going outside!\n'

IPNS 路径是'ipns/ QmVPUMd7mFG54zKDNNzPRgENsr5VTbBxWJThfVd6j9V4U8',但我们得到了不同的结果。

这非常有趣,但我们是否仅限于单个 IPNS 路径?不。您可以生成另一个密钥,以便您可以有另一个 IPNS 路径。

使用以下代码块创建一个名为generate_another_key.py的 Python 脚本:

import ipfsapi

c = ipfsapi.connect()
print(c.key_list())
c.key_gen('another_key', 'rsa')
print(c.key_list())

运行上述脚本将给出以下输出:

(ipfs-venv) $ python generate_another_key.py
{'Keys': [{'Name': 'self', 'Id': 'QmVPUMd7mFG54zKDNNzPRgENsr5VTbBxWJThfVd6j9V4U8'}]}
{'Keys': [{'Name': 'self', 'Id': 'QmVPUMd7mFG54zKDNNzPRgENsr5VTbBxWJThfVd6j9V4U8'}, {'Name': 'another_key', 'Id': 'QmcU8u2Koy4fdrSjnSEjrMRYZVPLKP5YXQhLVePfUmjmkv'}]}

您的新 IPNS 路径从'another_key''/ipns/ QmcU8u2Koy4fdrSjnSEjrMRYZVPLKP5YXQhLVePfUmjmkv'

然后,当您想要在 IPNS 路径上发布内容时,只需在name_publish()中使用key参数。使用以下代码块创建一个名为publish_horoscope1_in_another_ipns.py的脚本:

import ipfsapi

c = ipfsapi.connect()
peer_id = c.key_list()['Keys'][1]['Id']
c.name_publish('QmTG4eE6ruUDhSKxqwofJXXqDFAmNzQiGdo4Z7WvVdLZuS', key='another_key')
result = c.cat('/ipns/' + peer_id)
print(result)

运行它。您应该会得到第一个星座预测。正如您可能已经观察到的那样,我们使用了另一个对等 ID。注意peer_id = c.key_list()['Keys'][1]['Id']中的索引 1。之前,我们使用的是索引 0。

在 IPNS 中发布不会永久存储它。默认情况下,它会将 IPFS 文件存储 24 小时。您可以使用name_publish()中的lifetime关键字来更改持续时间。例如,如果您想要在 IPNS 中发布 IPFS 文件5h,您可以这样做:

c.name_publish('QmTG4eE6ruUDhSKxqwofJXXqDFAmNzQiGdo4Z7WvVdLZuS', key='another_key', lifetime='5h')

固定

如果您想要在 IPFS 上删除文件怎么办?假设您意外使用ipfs add命令添加了一张裸照:

$ ipfs add nude_picture.jpg
added QmWgMcTdPY9Rv7SCBusK1gWBRJcBi2MxNkC1yC6uvLYPwK nude_picture.jpg
2.64 MiB / 2.64 MiB [==================================================] 100.00%

如何删除您的裸照?没有ipfs rm QmPCqvJHUs517pdcFNZ7EJMKcjEtbUBUDYxZcwsSijRtBx这样的东西,因为这个命令没有任何意义。它应该做什么?告诉每个持有您裸照的节点删除照片?那将违背 IPFS 的崇高目的。您可以做的是删除 IPFS 本地存储中的照片。在 IPFS 中删除本地文件的术语称为删除固定。在删除裸照文件的固定之后,内容仍然在 IPFS 本地存储中。但是当 IPFS 的垃圾收集器工作以清理对象时,它将删除您裸照文件的内容。希望没有人有机会在他们的节点上固定(下载)这个敏感文件!

让我们创建一个脚本来删除固定并要求垃圾收集器执行其工作。将脚本命名为removing_nude_picture.py

import ipfsapi

c = ipfsapi.connect()
c.pin_rm('QmWgMcTdPY9Rv7SCBusK1gWBRJcBi2MxNkC1yC6uvLYPwK')
c.repo_gc()

运行脚本。然后,如果您尝试获取您的裸照的内容,将会失败:

(ipfs-venv) $ ipfs get QmWgMcTdPY9Rv7SCBusK1gWBRJcBi2MxNkC1yC6uvLYPwK

当然,如果有人已经在另一个节点上固定了您的裸照,那么您仍然会得到您的裸照的内容。

在这个例子中,裸照基本上是一张熊猫的裸照,可以从 Unsplash 网站下载:unsplash.com/photos/IgdVdJCmzf4。如果您在这个例子中使用这张图片,请准备好其他人也会使用它。要测试删除固定是否真的有效,您可以使用一个这个世界上没有人拥有的真正独特的文件。有一种方法可以通过解剖 IPFS 存储路径中的LevelDB文件来检查文件是否已从您的本地存储中删除。但是,这超出了本书的范围。

Pubsub

IPFS 有一个实验性功能,即发布-订阅,或pubsub。基本上,IPFS 中的一个节点可以订阅一个主题。假设这个主题是比特币主题。IPFS 中的一个节点可以向比特币主题发布*To the moon!*消息。然后,任何订阅'bitcoin'主题的节点都可以收到该消息。

因为 pubsub 是一个实验性功能,您需要使用特定标志运行 IPFS 守护程序。使用以下命令使用--enable-pubsub-experiment标志运行 IPFS 守护程序:

$ ipfs daemon --enable-pubsub-experiment

订阅者和发布者都需要使用特定标志运行守护程序。

让我们创建一个脚本来订阅特定主题,并将脚本命名为subscribe_topic.py

import ipfsapi
from base64 import b64decode

c = ipfsapi.connect()
with c.pubsub_sub('bitcoin') as sub:
    for message in sub:
        string = b64decode(message['data'])
        print(string)
        break

订阅主题的方法是pubsub_sub。第一个参数是我们想要订阅的主题。当我们从订阅中接收数据时,我们还将获得有关发送者的信息。但是,目前我们只关心消息。这条消息是以base64编码的,所以我们必须先解码它。

运行脚本。这将在接收到任何消息之前等待。

让我们创建一个脚本来发布到这个主题,并将脚本命名为publish_topic.py

import ipfsapi

c = ipfsapi.connect()
c.pubsub_pub('bitcoin', 'To the moon!')

最好在另一台计算机上运行此脚本,这样您就可以对去中心化技术的奇迹感到惊叹。不要忘记您必须使用特定标志运行 IPFS 守护程序。但是如果您懒得话,也可以在同一台计算机上运行此脚本。

当订阅脚本正在运行时,运行发布脚本。

然后,在运行订阅脚本的终端中,您应该会得到以下输出:

(ipfs-venv) $ python subscribe_topic.py
b'To the moon!'

那么,pubsub 有什么好处呢?首先,您可以构建一个通知系统。想象一下,您为家人运行一个家庭文件共享系统。当您添加您女儿的照片时,您希望通知所有家庭成员。不仅仅是通知;您还可以构建一个去中心化的聊天系统(类似于 IRC 或 Slack)。当与其他技术结合使用时,例如无内容复制数据类型,甚至可以在 IPFS 之上构建一个去中心化的在线配对编程系统!

请注意,pubsub 仍然是实验性功能。IPFS 的开发人员在计划中有许多有趣的计划。其中最有趣的计划之一是计划在 pubsub 系统的顶部添加基于密码学密钥的身份验证系统。现在,每个人都可以发布和订阅主题。

可变文件系统

IPFS 有一个名为可变文件系统(MFS)的功能。MFS 与您的操作系统文件系统不同。让我们创建一个脚本来探索这个功能,并将脚本命名为exploring_mfs.py

import ipfsapi
import io

c = ipfsapi.connect()

print("By default our MFS is empty.")
print(c.files_ls('/')) # root is / just like Unix filesystem

print("We can create a directory in our MFS.")
c.files_mkdir('/classical_movies')

print("We can create a file in our MFS.")
c.files_write('/classical_movies/titanic',
              io.BytesIO(b"The ship crashed. The end."),
              create=True)

print("We can copy a file in our MFS.")
c.files_cp('/classical_movies/titanic',
           '/classical_movies/copy_of_titanic')

print("We can read the file.")
print(c.files_read('/classical_movies/titanic'))

print("We can remove the file.")
print(c.files_rm('/classical_movies/copy_of_titanic'))

print("Now our MFS is not empty anymore.")
print(c.files_ls('/'))
print(c.files_ls('/classical_movies'))

print("You can get the hash of a path in MFS.")
print(c.files_stat('/classical_movies'))

print("Then you can publish this hash into IPNS.")

运行这将给出以下输出:

(ipfs-venv) $ python exploring_mfs.py
By default our MFS is empty.
{'Entries': None}
We can create a directory in our MFS.
We can create a file in our MFS.
We can copy a file in our MFS.
We can read the file.
b'The ship crashed. The end.'
We can remove the file.
[]
Now our MFS is not empty anymore.
{'Entries': [{'Name': 'classical_movies', 'Type': 0, 'Size': 0, 'Hash': ''}]}
{'Entries': [{'Name': 'titanic', 'Type': 0, 'Size': 0, 'Hash': ''}]}
You can get the hash of a path in MFS.
{'Hash': 'QmNUrujevkYqRtYmpaj2Af1DvgR8rt9a7ApyiyXHnF5wym', 'Size': 0, 'CumulativeSize': 137, 'Blocks': 1, 'Type': 'directory'}
Then you can publish this hash into IPNS

您可能会想知道这个功能的意义是什么。在您的 MFS 中,没有任何您不能在操作系统文件系统中做的事情。在这个特定的例子中,是的,您是对的,这个功能是毫无意义的。但是在复制文件时,MFS 和您的操作系统文件系统之间有一个微妙的区别。让我们创建一个脚本来证明这个断言,并将脚本命名为copy_in_mfs.py

import ipfsapi

c = ipfsapi.connect()
c.files_cp('/ipfs/QmY8zTocoVNDJWUr33nhksBiZ3hxugFPhb6qSzpE761bVN', '/46MB_cute_bear.mp4')

print(c.files_ls('/'))

运行脚本将给出以下输出:

(ipfs-venv) $ python copy_in_mfs.py 
{'Entries': [{'Name': '46MB_cute_bear.mp4', 'Type': 0, 'Size': 0, 'Hash': ''}, {'Name': 'classical_movies', 'Type': 0, 'Size': 0, 'Hash': ''}]}

具有哈希链接QmY8zTocoVNDJWUr33nhksBiZ3hxugFPhb6qSzpE761bVN的文件是一个可爱的熊视频,可以从以下链接下载:videos.pexels.com/videos/bear-in-a-forest-855113。您可以使用以下命令下载:ipfs get QmY8zTocoVNDJWUr33nhksBiZ3hxugFPhb6qSzpE761bVN(假设我的 IPFS 节点在线,您可以从该 URL 下载视频并将视频固定在另一台计算机上以测试脚本,如果没有其他固定此视频的 IPFS 节点在线)。文件大小为 46 MB,但脚本执行非常快。考虑到我们必须下载视频文件,脚本的运行时间太快了。这是因为我们没有将视频下载到我们的存储中。在我们的 MFS 中/46MB_cute_bear.mp4的路径不是真正的传统文件,就像我们的操作系统文件系统中一样。您可以说它就像是指向 IPFS 中真实文件的符号链接,由 IPFS 生态系统中的一些节点固定。

这意味着您可以从 IPFS 路径复制 100 TB 文件到您的 MFS,而不会占用任何存储空间(除了一些元数据)。

如果您像计算机科学家一样思考,IPFS 文件系统就像一个巨大的图形数据库。

其他 API

IPFS HTTP API 还有其他我们没有空间讨论的方法。完整的参考资料可以在docs.ipfs.io/reference/api/http/中找到。有 API 可以引导您的节点(如果您想要基于某些现有节点构建您的节点列表),从节点中找到附近的节点,连接到特定节点,配置 IPFS,关闭 IPFS 守护程序等。

摘要

在本章中,您已经学会了如何使用 Python 通过 HTTP API 与 IPFS 进行交互。首先,您安装了 IPFS 软件并运行了守护进程。您通过将文件添加到 IPFS 并学习如何获取文件内容的哈希值(基于 protobuf、multihash 和 base58)来开始这一过程。然后,您看到如果将大文件添加到 IPFS 中,它将被分成许多块。您还可以将文件目录添加到 IPFS 中。基于这种能力,您可以在 IPFS 上托管静态网站。然后,您了解了如何在 IPNS 上发布 IPFS 文件,从而可以拥有动态内容。之后,您了解了 MFS,可以在其中从 IPFS 复制大文件,而不会在本地存储中产生任何显著的成本。

在下一章中,您将结合 IPFS 和智能合约来构建一个去中心化的应用程序。

进一步阅读

以下是与本章相关的各个网站的参考资料:

第十二章:使用 IPFS 实现去中心化应用

在本章中,我们将结合智能合约和星际文件系统IPFS)来构建一个去中心化的视频分享应用(类似于 YouTube 但是去中心化)。我们将使用一个 Web 应用作为区块链和 IPFS 的前端。正如之前所述,IPFS 并不是一种区块链技术。IPFS 是一种去中心化技术。然而,在区块链论坛、聚会或教程中,你可能会经常听到 IPFS 被提到。其中一个主要原因是 IPFS 克服了区块链的弱点,即其存储非常昂贵。

在本章中,我们将涵盖以下主题:

  • 去中心化视频分享应用的架构

  • 编写视频分享智能合约

  • 构建视频分享 Web 应用

去中心化视频分享应用的架构

这是我们的应用在完成后的样子——首先,你去一个网站,在那里你会看到一个视频列表(就像 YouTube 一样)。在这里,你可以在浏览器中播放视频,上传视频到你的浏览器,这样人们就可以观看你的可爱的猫视频,并喜欢其他人的视频。

从表面上看,这就像一个普通的应用。你可以用你喜欢的 Python Web 框架构建它,比如 Django、Flask 或 Pyramid。然后你可以使用 MySQL 或 PostgreSQL 作为数据库。你可以选择 NGINX 或 Apache 作为 Gunicorn Web 服务器前面的 Web 服务器。对于缓存,你可以使用 Varnish 进行整页缓存,使用 Redis 进行模板缓存。你还将在云上托管 Web 应用和视频,比如亚马逊网络服务AWS)或谷歌云平台GCP),Azure。然后你将使用内容传送网络使其在全球范围内可扩展。对于前端,你可以使用 JavaScript 框架,如 React.js、Angular.js、Vue.js 或 Ember。如果你是一个高级用户,你可以使用机器学习进行视频推荐。

然而,关键点在于,我们想要构建的是一个利用区块链技术的去中心化视频分享应用,而不是一个集中式应用。

让我们讨论一下我们所说的利用区块链技术构建去中心化视频分享应用的含义。

我们无法在以太坊区块链上存储视频文件,因为这非常昂贵;即使在以太坊区块链上存储图片文件也要花费很多钱。有人已经为我们在以下链接上进行了计算:ethereum.stackexchange.com/questions/872/what-is-the-cost-to-store-1kb-10kb-100kb-worth-of-data-into-the-ethereum-block

存储 1 KB 的成本大约为 0.032 ETH。一个体面的图像文件大约为 2 MB。如果你问硬盘制造商,1 MB 就是 1,000 KB,如果你问操作系统,就是 1,024 KB。我们简单地将其四舍五入为 1,000,因为这对我们的计算没有任何影响。因此,在以太坊上存储 2 MB 文件的成本大约是 2,000 乘以 0.032 ETH,等于 64 ETH。ETH 的价格一直在变化。在撰写本文时,1 ETH 的成本大约是 120 美元。这意味着要存储一个 2 MB 的图片文件(Unsplash 网站上的普通大小库存图片文件),你需要花费 7,680 美元。一个一分半钟的 MP4 格式视频文件大约为 46 MB。因此,你需要花费 176,640 美元来在以太坊上存储这个视频文件。

与其支付这个费用,区块链开发者通常会在区块链上存储视频文件的引用,并将视频文件本身存储在正常的存储介质上,比如 AWS。在 Vyper 智能合约中,你可以使用bytes数据类型:

cute_panda_video: bytes[128]

然后,你可以在智能合约中存储你在 AWS S3(aws.amazon.com/s3/)中存储的视频链接:

cute_panda_video = "http://abucket.s3-website-us-west-2.amazonaws.com/cute_panda_video.mp4"

这种方法都很好,但问题在于你依赖于 AWS。如果公司不喜欢你的可爱熊猫视频,他们可以删除它,而存在于智能合约中的 URL 也会变得无效。当然,你可以改变智能合约中cute_panda_video变量的值(除非你禁止这样做)。然而,这种情况会给我们的应用程序带来不便。如果你使用来自集中化公司的服务,你的命运取决于该公司的心情。

我们可以通过使用 IPFS 等去中心化存储来缓解这个问题。我们可以将 IPFS 路径(或 IPFS 哈希)存储为cute_panda_video变量的值,类似于以下示例:

cute_panda_video = "/ipfs/QmWgMcTdPY9Rv7SCBusK1gWBRJcBi2MxNkC1yC6uvLYPwK"

然后,我们可以在 AWS 和其他地方(比如 GCP)上启动我们的 IPFS 守护程序。因此,如果 AWS 审查我们的可爱熊猫视频,我们的可爱熊猫视频的 IPFS 路径仍然有效。我们可以从其他地方(比如 GCP)提供视频。你甚至可以将视频托管在你奶奶家的电脑上。痴迷于可爱熊猫视频的人甚至可以固定该视频,帮助我们提供可爱熊猫视频。

除了以去中心化的方式托管可爱的熊猫视频之外,去中心化视频分享应用程序还有其他价值。这个价值与区块链技术有关。假设我们想要构建一个视频点赞功能。我们可以将点赞值存储在区块链上。这可以防止腐败。想象一下,我们想要为最可爱的熊猫视频举办一个投票比赛,奖金是 10 个比特币。如果我们的比赛应用程序是以集中化的方式完成的(使用表将点赞值存储在 SQL 数据库中,比如 MySQL 或 PostgreSQL),我们作为集中化的管理员可以使用以下代码操纵获胜者:

UPDATE thumbs_up_table SET aggregate_voting_count = 1000000 WHERE video_id = 234;

当然,作弊并不是这么容易的。你需要通过确保聚合计数与个体计数匹配来掩盖你的行踪,这需要做得很微妙。你可以在一个小时内将聚合计数添加到 100 到 1,000 之间的随机数字,而不是一次性添加一百万张选票,这并不是在建议你欺骗用户,我只是在阐明我的观点。

通过区块链,我们可以防止集中化管理员对数据完整性的腐败。点赞值保存在智能合约中,并且你可以让人们审计智能合约的源代码。我们在去中心化视频分享应用程序上的点赞功能通过一个诚实的过程增加了视频的点赞数。

除了数据的完整性,我们还可以构建一个加密经济。我的意思是,我们可以在我们的智能合约中进行经济活动(比如出售、购买、竞标等)。我们可以在同一个智能合约中构建代币。这个代币的硬币可以用来点赞视频,这样点赞视频就不再是免费的。视频的所有者可以像把钱放进口袋一样把它兑现出来。这种动态可以激励人们上传更好的视频。

除此之外,去中心化的应用程序保证了 API 的独立性。应用程序的去中心化性质防止了 API 受到类似 Twitter API 丑闻的干扰或骚扰。很久以前,开发者可以在 Twitter API 的基础上自由开发有趣的应用程序,但后来 Twitter 对开发者如何使用他们的 API 施加了严格的限制。其中一个例子是 Twitter 曾经关闭了 Politwoops 对 API 的访问权限,该应用程序保存了政客删除的推文。不过现在访问权限已经重新激活。通过使我们的应用程序去中心化,我们可以增加 API 的民主性质。

出于教育目的,我们的应用程序有两个主要功能。首先,你可以看到视频列表,播放视频和上传视频。这些都是你在 YouTube 上做的正常事情。其次,你可以点赞视频,但只能用硬币或代币。

在着手构建应用程序之前,让我们设计智能合约的架构和 Web 应用程序的架构。

视频分享智能合约的架构

我们的应用程序始于智能合约。我们的智能合约需要做一些事情,具体如下:

  • 跟踪用户上传的视频

  • 利用代币及其标准操作(ERC20)

  • 提供一种用户可以使用硬币或代币点赞视频的方式

  • 用于点赞视频的硬币将转移到视频所有者

就是这样。我们始终努力使智能合约尽可能简短。代码行数越多,出现错误的机会就越大。而智能合约中的错误是无法修复的。

在编写这个智能合约之前,让我们考虑一下我们想要如何构建智能合约。智能合约的结构包括数据结构。让我们看一个例子,我们想要使用什么数据结构来跟踪用户的视频。

我们肯定想要使用一个映射变量,其中地址数据类型作为键。这里的困难部分是选择我们想要用作这个映射数据类型值的数据类型。正如我们在第三章中学到的,Vyper 中没有无限大小的数组。如果我们使用bytes32数组,我们将受限于作为这个映射值的数组的特定大小。这意味着用户最多可以拥有一定大小的视频。我们可以使用bytes32数组来保存非常大的视频列表,比如 100 万个视频。有人会上传超过 100 万个视频的机会有多大呢?如果你每天上传一个视频,十年内你只会上传 3650 个视频。然而,bytes32数组的问题是,它不能接受超过 32 字节大小的数据。IPFS 路径,比如QmWgMcTdPY9Rv7SCBusK1gWBRJcBi2MxNkC1yC6uvLYPwK,长度为 44 个字符。因此,你必须至少使用bytes[44]数据类型,但我们将其四舍五入为bytes[50]

相反,我们希望有另一个映射数据类型变量(让我们称之为映射 z)作为前一段描述的这个映射数据类型变量的值。映射 z 的键是整数,值是包含bytes[50]数据类型变量以保存 IPFS 路径和bytes[20]数据类型变量以保存视频标题的结构。有一个整数跟踪器来初始化映射 z 中键的值。这个整数跟踪器的初始值为 0。每次我们向映射 z 添加一个视频(IPFS 路径和视频标题),我们就将这个整数跟踪器加一。因此,下次我们添加另一个视频时,映射 z 的键不再是 0,而是 1。这个整数跟踪器对每个账户都是唯一的。我们可以创建另一个映射将账户映射到这个整数跟踪器。

在处理视频之后,我们关注点赞。我们如何存储用户 A 点赞视频 Z 的事实?我们需要确保用户不能多次点赞同一视频。最简单的方法是创建一个映射,其中bytes[100]数据类型作为键,boolean数据类型作为值。bytes[100]数据类型变量是使用视频点赞者的地址、视频上传者的地址和视频索引的组合。boolean数据类型变量用于指示用户是否已经点赞了视频。

此外,我们需要一个整数数据类型来保持视频点赞数量的总计。总点赞数是一个映射,其中bytes[100]数据类型作为键,integer数据类型作为值。bytes[100]数据类型变量是视频上传者的地址和视频索引的组合。

这种方法的缺点是很难跟踪哪些用户喜欢了智能合约中的特定视频。我们可以创建另一个映射来跟踪哪些用户喜欢了某个视频。然而,这会使我们的智能合约变得更加复杂。之前,我们不惜一切代价创建了一个专门用于跟踪用户上传的所有视频的映射。这是必要的,因为我们想要获取用户视频的列表。这就是我们所说的核心功能。然而,跟踪哪些用户喜欢了一个视频并不是我所说的核心功能。

只要我们能够让视频点赞的过程变得诚实,我们就不需要跟踪哪些用户喜欢了一个视频。如果我们真的渴望跟踪这些用户,我们可以在智能合约中使用事件。每当用户喜欢一个视频,它就会触发一个事件。然后,在客户端使用web3.py库,我们可以过滤这些事件以获取所有喜欢特定视频的用户。这将是一个昂贵的过程,应该单独完成主要应用程序。我们可以使用 Celery 进行后台作业,此时结果可以存储在数据库中,如 SQlite、PostgreSQL 或 MySQL。构建去中心化应用并不意味着完全否定集中化的方法。

有关代币的主题已在第八章中进行了彻底讨论,在以太坊中创建代币

视频共享 Web 应用程序的架构

我们将开发一个 Python Web 应用程序,用作我们智能合约的前端。这意味着我们需要一个适当的服务器来成为 Python Web 应用程序的主机。为此,我们至少需要一个 Gunicorn Web 服务器。换句话说,我们需要在集中服务器上托管我们的 Python Web 应用程序,例如在 AWS、GCP 或 Azure 中。这对于观看视频来说是可以的,但当用户想要上传视频时就会出现问题,因为这需要访问私钥。用户可能会担心我们在集中服务器上的 Python Web 应用程序会窃取他们的私钥。

因此,解决方案是将我们的 Python Web 应用程序的源代码发布在 GitHub 或 GitLab 上,然后告诉用户下载、安装并在他们的计算机上运行它。他们可以审计我们的 Python Web 应用程序的源代码,以确保没有恶意代码试图窃取他们的私钥。然而,如果他们需要每次审计源代码,那么我们就在 Git 存储库上添加另一个提交。

或者更好的是,我们可以将我们的 Python Web 应用程序的源代码存储在 IPFS 上。他们可以从 IPFS 下载这个源代码,并确保我们应用程序的源代码不会被篡改。他们只需要在使用之前审计一次源代码。

然而,虽然我们可以在 IPFS 上托管静态网站,但我们无法对 Python、PHP、Ruby 或 Perl Web 应用程序等动态网页执行相同的操作。这些动态网站需要一个适当的 Web 服务器。因此,任何下载我们的 Python Web 应用程序源代码的人都需要在执行我们的应用程序之前安装正确的软件。他们需要安装 Python 解释器、Web 服务器(Gunicorn、Apache 或 NGINX)以及所有必要的库。

然而,只有桌面用户才能这样做。移动用户无法执行我们的应用程序,因为 Android 或 iOS 平台上没有适当的 Python 解释器或 Web 服务器。

这就是 JavaScript 的亮点所在。您可以创建一个静态网站,使其具有动态性,以便在网页中实现交互。您还可以使用 React.js、Angular.js、Ember.js 或 Vue.js 创建一个复杂的 JavaScript Web 应用程序,并将其部署在 IPFS 上。桌面用户和移动用户都可以执行 JavaScript Web 应用程序。因为这是一本关于 Python 的书,我们仍然会考虑创建一个 Python Web 应用程序。但是,您应该记住 JavaScript 相对于 Python 的优势。

无论 JavaScript 有多好,它仍然不能解决移动用户的困境。移动平台上的计算能力仍然不如桌面平台上的计算能力强大。你仍然不能在移动平台上运行完整的以太坊节点,就像你不能在移动平台上运行 IPFS 软件一样。

让我们设计我们的 Web 应用程序。这有一些实用工具:

  • 播放视频

  • 上传视频

  • 点赞视频

  • 从许多用户中列出最近的视频

  • 列出一个特定用户的所有视频

列出一个特定用户的所有视频相对容易,因为在智能合约中,我们有一个无限大小的数组(基本上是一个以整数为键和另一个整数跟踪器的映射),我们可以根据用户获取视频。页面的控制器接受一个用户(或者基本上是智能合约中的地址)作为参数。

播放视频接受视频上传者的地址和视频索引作为参数。如果视频还没有存在于我们的存储中,我们会从 IPFS 上下载它。然后我们将视频提供给用户。

上传视频需要与以太坊节点进行交互。上传视频的方法或功能接受一个要使用的账户地址的参数,一个加密私钥的密码参数,一个视频文件的参数,以及一个视频标题的参数。我们首先将视频文件存储在 IPFS 上。然后如果成功,我们可以在区块链上存储关于这个视频的信息。

点赞视频也需要与以太坊节点进行交互。点赞视频的方法或功能接受一个视频点赞者的地址参数,一个加密私钥的密码参数,一个视频上传者的地址参数,以及一个视频索引的参数。在确保用户之前没有点赞视频的情况下,我们将信息存储在区块链上。

从许多用户中列出最近的视频有点棘手。所涉及的工作量相当大。在智能合约中,我们没有一个变量来跟踪所有参与用户。我们也没有一个变量来跟踪不同用户的所有视频。然而,我们可以通过在区块链上存储视频信息的方法创建一个事件。这样做之后,我们可以从这个事件中找到所有最近的视频。

现在是时候构建去中心化的视频分享应用程序了。

编写视频分享智能合约

话不多说,让我们设置我们的智能合约开发平台:

  1. 首先,我们按照以下方式设置我们的虚拟环境:
$ virtualenv -p python3.6 videos-venv
$ source videos-venv/bin/activate
(videos-venv) $
  1. 然后我们安装 Web3、Populus 和 Vyper:
(videos-venv) $ pip install eth-abi==1.2.2
(videos-venv) $ pip install eth-typing==1.1.0
(videos-venv) $ pip install py-evm==0.2.0a33
(videos-venv) $ pip install web3==4.7.2
(videos-venv) $ pip install -e git+https://github.com/ethereum/populus#egg=populus
(videos-venv) $ pip install vyper 

Vyper 的最新版本是 0.1.0b6,这破坏了 Populus。开发者需要一些时间来修复这个问题。如果在你阅读本书时 bug 还没有被修复,你可以自己修补 Populus。

  1. 使用以下命令检查这个库是否已经修复了 bug:
(videos-venv) $ cd videos-venv/src/populus
(videos-venv) $ grep -R "compile(" populus/compilation/backends/vyper.py
 bytecode = '0x' + compiler.compile(code).hex()
 bytecode_runtime = '0x' + compiler.compile(code, bytecode_runtime=True).hex()

在我们的情况下,bug 还没有被修复。

  1. 所以,让我们修补 Populus 以修复 bug。确保你仍然在同一个目录中(videos-venv/src/populus):
(videos-venv) $ wget https://patch-diff.githubusercontent.com/raw/ethereum/populus/pull/484.patch
(videos-venv) $ git apply 484.patch
(videos-venv) $ cd ../../../ 
  1. 在修补 Populus 之后,我们将创建我们的智能合约项目目录:
(videos-venv) $ mkdir videos-sharing-smart-contract
  1. 然后,我们将目录初始化为 Populus 项目目录:
(videos-venv) $ cd videos-sharing-smart-contract
(videos-venv) $ mkdir contracts tests 
  1. 接下来,我们将在 Populus 项目目录中下载 Populus 配置文件:
(videos-venv) $ wget https://raw.githubusercontent.com/ethereum/populus/master/populus/assets/defaults.v9.config.json -O project.json
  1. 我们现在将打开 Populus 的project.json配置文件,并覆盖compilation键的值,如下面的代码块所示:
  "compilation": {
    "backend": {
      "class": "populus.compilation.backends.VyperBackend"
    },
    "contract_source_dirs": [
      "./contracts"
    ],
    "import_remappings": []
  },
  1. 然后我们在videos-sharing-smart-contract/contracts/VideosSharing.vy中编写我们的智能合约代码,如下面的代码块所示(请参考以下 GitLab 链接的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/videos_sharing_smart_contract/contracts/VideosSharing.vy):
struct Video:
    path: bytes[50]
    title: bytes[20]

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
UploadVideo: event({_user: indexed(address), _index: uint256})
LikeVideo: event({_video_liker: indexed(address), _video_uploader: indexed(address), _index: uint256})

...
...

@public
@constant
def video_aggregate_likes(_user_video: address, _index: uint256) -> uint256:
    _user_video_str: bytes32 = convert(_user_video, bytes32)
    _index_str: bytes32 = convert(_index, bytes32)
    _key: bytes[100] = concat(_user_video_str, _index_str)

    return self.aggregate_likes[_key]

现在,让我们逐步讨论我们的智能合约:

struct Video:
    path: bytes[50]
    title: bytes[20]

这是我们想要在区块链上保留的视频信息的结构体。Video结构体的path存储了 IPFS 路径,长度为 44。如果我们使用另一个哈希函数,IPFS 路径将有不同的长度。请记住,IPFS 在对对象进行哈希时使用多哈希。如果在 IPFS 配置中使用更昂贵的哈希函数,比如 SHA512,那么需要将bytes[]数组数据类型的大小加倍。例如,bytes[100]应该足够了。Video结构体的title存储了视频标题。在这里,我使用了bytes[20],因为我想要标题简短。如果你想要存储更长的标题,可以使用更长的字节,比如bytes[100]。然而,请记住,在区块链上存储的字节数越多,需要花费的 gas(费用)就越多。当然,你可以在这个结构体中添加更多信息,比如视频描述或视频标签,只要你知道后果,即执行存储视频信息的方法需要更多的 gas。

我们现在转移到事件列表:

Transfer: event({_from: indexed(address), _to: indexed(address), _value: uint256})
Approval: event({_owner: indexed(address), _spender: indexed(address), _value: uint256})
UploadVideo: event({_user: indexed(address), _index: uint256})
LikeVideo: event({_video_liker: indexed(address), _video_uploader: indexed(address), _index: uint256})

TransferApproval是 ERC20 标准事件的一部分。你可以在第八章中了解更多关于 ERC20 的信息,在以太坊中创建代币UploadVideo事件在我们的智能合约中上传视频信息时触发。我们保存视频上传者的地址和视频的索引。LikeVideo事件在我们的智能合约中喜欢视频时触发。

我们保存视频喜欢者的地址,视频上传者的地址和视频的索引:

user_videos_index: map(address, uint256)

这是我们无限数组的整数跟踪器。所以如果user_videos_index[用户 A 的地址] = 5,这意味着用户 A 已经上传了四个视频。

以下是 ERC20 标准的一部分:

name: public(bytes[20])
symbol: public(bytes[3])
totalSupply: public(uint256)
decimals: public(uint256)
balances: map(address, uint256)
allowed: map(address, map(address, uint256))

有关 ERC20 的更多信息,请参阅第八章,在以太坊中创建代币

我们继续下一行:

all_videos: map(address, map(uint256, Video))

这是保留所有用户所有视频的核心变量。address数据类型键用于保存用户的地址。map(uint256, Video)数据类型值是我们的无限数组。map(uint256, Video)中的uint256键从 0 开始,然后由user_videos_index变量跟踪。Video结构体是我们的视频信息。

接下来的两行代码用于喜欢:

likes_videos: map(bytes[100], bool)
aggregate_likes: map(bytes[100], uint256)

likes_videos变量是用来检查某个用户是否喜欢特定视频的变量。aggregate_likes变量是用来显示这个特定视频已经获得了多少赞。

我们现在已经定义了变量,将继续下面代码块中的代码:

@public
def __init__():
    _initialSupply: uint256 = 500
    _decimals: uint256 = 3
    self.totalSupply = _initialSupply * 10 ** _decimals
    self.balances[msg.sender] = self.totalSupply
    self.name = 'Video Sharing Coin'
    self.symbol = 'VID'
    self.decimals = _decimals
    log.Transfer(ZERO_ADDRESS, msg.sender, self.totalSupply)

...
...

@public
@constant
def allowance(_owner: address, _spender: address) -> uint256:
    return self.allowed[_owner][_spender]

这是标准的 ERC20 代码,你可以在第八章中了解更多,在以太坊中创建代币。然而,我对代码进行了小幅调整,如下面的代码块所示:

@private
def _transfer(_source: address, _to: address, _amount: uint256) -> bool:
    assert self.balances[_source] >= _amount
    self.balances[_source] -= _amount
    self.balances[_to] += _amount
    log.Transfer(_source, _to, _amount)

    return True

@public
def transfer(_to: address, _amount: uint256) -> bool:
    return self._transfer(msg.sender, _to, _amount)

在这个智能合约中,我将transfer方法的内部代码提取到了专用的私有方法中。这样做的原因是,转移代币的功能将在喜欢视频的方法中使用。记住,当我们喜欢一个视频时,我们必须向视频上传者支付代币。我们不能在另一个公共函数中调用公共函数。其余的代码是一样的(除了代币的名称):

@public
def upload_video(_video_path: bytes[50], _video_title: bytes[20]) -> bool:
    _index: uint256 = self.user_videos_index[msg.sender]

    self.all_videos[msg.sender][_index] = Video({ path: _video_path, title: _video_title })
    self.user_videos_index[msg.sender] += 1

    log.UploadVideo(msg.sender, _index)aggregate_likes

    return True

这是用于在区块链上存储视频信息的方法。我们在将视频上传到 IPFS 后调用这个方法。_video_path是 IPFS 路径,_video_title是视频标题。我们从视频上传者(msg.sender)那里获取最新的索引。然后我们根据视频上传者的地址和最新的索引将Video结构体的值设置为all_videos

然后我们增加整数跟踪器(user_videos_index)。不要忘记记录这个事件。

@public
@constant
def latest_videos_index(_user: address) -> uint256:
    return self.user_videos_index[_user]

@public
@constant
def videos_path(_user: address, _index: uint256) -> bytes[50]:
    return self.all_videos[_user][_index].path

@public
@constant
def videos_title(_user: address, _index: uint256) -> bytes[20]:
    return self.all_videos[_user][_index].title

在前面的代码块中的方法是用于客户端使用 web3 获取最新视频索引、视频 IPFS 路径和视频标题的便利方法。没有这些方法,你仍然可以获取视频的信息,但是使用 web3 访问嵌套映射数据类型变量中的结构变量并不直接。

以下代码显示了用于点赞视频的方法。它接受视频上传者的地址和视频索引。在这里,你创建了两个键——一个用于likes_videos,另一个用于aggregate_likeslikes_videos的键是视频点赞者的地址、视频上传者的地址和视频索引的组合。aggregate_likes的键是视频上传者的地址和视频索引的组合。创建键之后,我们确保视频点赞者将来不能再次点赞同一个视频,并且视频点赞者之前没有点赞过这个特定视频。点赞视频只是将likes_videos变量与我们创建的键设置为True。然后我们将aggregate_likes的值增加 1。最后,我们将代币中的一枚硬币从视频点赞者转移到视频上传者。不要忘记记录这个事件:

@public
def like_video(_user: address, _index: uint256) -> bool:
    _msg_sender_str: bytes32 = convert(msg.sender, bytes32)
    _user_str: bytes32 = convert(_user, bytes32)
    _index_str: bytes32 = convert(_index, bytes32)
    _key: bytes[100] = concat(_msg_sender_str, _user_str, _index_str)
    _likes_key: bytes[100] = concat(_user_str, _index_str)
 a particular
    assert _index < self.user_videos_index[_user]
    assert self.likes_videos[_key] == False

    self.likes_videos[_key] = True
    self.aggregate_likes[_likes_key] += 1
    self._transfer(msg.sender, _user, 1)

    log.LikeVideo(msg.sender, _user, _index)

    return True

以下代码行是用于检查特定用户是否已经点赞视频以及这个特定视频已经获得多少赞的便利方法:

@public
@constant
def video_has_been_liked(_user_like: address, _user_video: address, _index: uint256) -> bool:
    _user_like_str: bytes32 = convert(_user_like, bytes32)
    _user_video_str: bytes32 = convert(_user_video, bytes32)
    _index_str: bytes32 = convert(_index, bytes32)
    _key: bytes[100] = concat(_user_like_str, _user_video_str, _index_str)

    return self.likes_videos[_key]

@public
@constant
def video_aggregate_likes(_user_video: address, _index: uint256) -> uint256:
    _user_video_str: bytes32 = convert(_user_video, bytes32)
    _index_str: bytes32 = convert(_index, bytes32)
    _key: bytes[100] = concat(_user_video_str, _index_str)

    return self.aggregate_likes[_key]

让我们在videos_sharing_smart_contract/tests/test_video_sharing.py中编写一个测试。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/videos_sharing_smart_contract/tests/test_videos_sharing.py

import pytest
import eth_tester

def upload_video(video_sharing, chain, account, video_path, video_title):
    txn_hash = video_sharing.functions.upload_video(video_path, video_title).transact({'from': account})
    chain.wait.for_receipt(txn_hash)

def transfer_coins(video_sharing, chain, source, destination, amount):
    txn_hash = video_sharing.functions.transfer(destination, amount).transact({'from': source})
    chain.wait.for_receipt(txn_hash)

...
...

   assert events[1]['args']['_video_liker'] == video_liker2
    assert events[1]['args']['_video_uploader'] == video_uploader
    assert events[1]['args']['_index'] == 0

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        like_video(video_sharing, chain, video_liker, video_uploader, 0)

让我们逐步详细讨论测试脚本。在下面的代码块中,导入必要的库之后,我们创建了三个便利函数——一个用于上传视频的函数,一个用于转移代币的函数,以及一个用于点赞视频的函数:

import pytest
import eth_tester

def upload_video(video_sharing, chain, account, video_path, video_title):
    txn_hash = video_sharing.functions.upload_video(video_path, video_title).transact({'from': account})
    chain.wait.for_receipt(txn_hash)

def transfer_coins(video_sharing, chain, source, destination, amount):
    txn_hash = video_sharing.functions.transfer(destination, amount).transact({'from': source})
    chain.wait.for_receipt(txn_hash)

def like_video(video_sharing, chain, video_liker, video_uploader, index):
    txn_hash = video_sharing.functions.like_video(video_uploader, index).transact({'from': video_liker})
    chain.wait.for_receipt(txn_hash)

如下面的代码块所示,在上传视频之前,我们确保最新视频的索引为 0。然后,在上传一个视频之后,我们应该检查最新视频的索引,这个索引应该增加 1。当然,我们也要检查视频路径和视频标题。然后我们再次上传一个视频并检查最新视频的索引,这时应该是 2。我们还要检查视频路径和视频标题。最后,我们检查事件,并确保它们已经被正确创建:

def test_upload_video(web3, chain):
    video_sharing, _ = chain.provider.get_or_deploy_contract('VideosSharing')

    t = eth_tester.EthereumTester()
    video_uploader = t.get_accounts()[1]

    index = video_sharing.functions.latest_videos_index(video_uploader).call()
    assert index == 0

...
...

    assert events[0]['args']['_user'] == video_uploader
    assert events[0]['args']['_index'] == 0

    assert events[1]['args']['_user'] == video_uploader
    assert events[1]['args']['_index'] == 1

让我们看一下测试脚本的下一部分:

def test_like_video(web3, chain):
    video_sharing, _ = chain.provider.get_or_deploy_contract('VideosSharing')

    t = eth_tester.EthereumTester()
    manager = t.get_accounts()[0]
    video_uploader = t.get_accounts()[1]
    video_liker = t.get_accounts()[2]
    video_liker2 = t.get_accounts()[3]

    transfer_coins(video_sharing, chain, manager, video_liker, 100)
    transfer_coins(video_sharing, chain, manager, video_liker2, 100)
    transfer_coins(video_sharing, chain, manager, video_uploader, 50)
    upload_video(video_sharing, chain, video_uploader, b'video-ipfs-path', b"video title")

...
...

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        like_video(video_sharing, chain, video_liker, video_uploader, 0)

首先,我们从管理员账户(启动智能合约的账户)向不同的账户转移一些代币,然后我们上传一个视频。在点赞视频之前,我们应该确保账户的代币余额是正确的,测试账户还没有点赞这个视频,并且累计点赞数仍然是 0。

完成这些之后,我们从特定账户点赞一个视频。视频点赞者的代币余额应该减少 1,视频上传者的代币余额应该增加 1。这意味着智能合约已记录下这个账户点赞了视频,并且这个视频的累计点赞数应该增加 1。

然后,我们从另一个账户点赞一个视频。视频点赞者的代币余额应该减少 1,视频上传者的代币余额应该再次增加 1。智能合约已记录下另一个账户点赞了这个视频,此时这个视频的累计点赞数应该再次增加 1,变为 2。

然后,我们确保视频点赞事件被触发。

最后,我们确保视频点赞者不能多次点赞同一视频。

我们不会讨论智能合约的 ERC20 部分的测试。请参考第八章,在以太坊中创建代币,了解如何测试 ERC20 代币智能合约。

要执行测试,请运行以下语句:

(videos-venv) $ py.test tests/test_videos_sharing.py

启动私有以太坊区块链

让我们使用 geth 启动我们的私有以太坊区块链。我们在这里不使用 Ganache,因为稳定版本的 Ganache 尚不支持事件(然而,Ganache 的 beta 版本(v 2.0.0 beta 2)已经支持事件):

  1. 我们将使用以下代码块来启动区块:
(videos-venv) $ cd videos_sharing_smart_contract
(videos-venv) $ populus chain new localblock
(videos-venv) $ ./chains/localblock/init_chain.sh
  1. 现在编辑chains/localblock/run_chain.sh。找到--ipcpath,然后更改值(--ipcpath后面的单词)为/tmp/geth.ipc

  2. 然后编辑project.json文件。chains对象指向四个键:testertempropstenmainnet。在chains对象中添加另一个键localblock

    "localblock": {
      "chain": {
        "class": "populus.chain.ExternalChain"
      },
      "web3": {
        "provider": {
          "class": "web3.providers.ipc.IPCProvider",
        "settings": {
          "ipc_path":"/tmp/geth.ipc"
        }
       }
      },
      "contracts": {
        "backends": {
          "JSONFile": {"$ref": "contracts.backends.JSONFile"},
          "ProjectContracts": {
            "$ref": "contracts.backends.ProjectContracts"
          }
        }
      }
    }
  1. 使用以下命令运行区块链:
(videos-venv) $ ./chains/localblock/run_chain.sh
  1. 使用以下命令编译我们的智能合约:
(videos-venv) $ populus compile
  1. 然后,使用以下命令将我们的智能合约部署到我们的私有区块链:
(videos-venv) $ populus deploy --chain localblock VideosSharing

将我们的智能合约部署的地址写入address.txt。该文件必须与videos_sharing_smart_contract目录相邻。

创建引导脚本

此脚本用于加载数据,以便更轻松地开发我们的应用程序。我们可以从videos.pexels.com/下载免费视频。在videos_sharing_smart_contract目录旁边创建一个stock_videos目录,并将一些 MP4 文件下载到该stock_videos目录中。在我的情况下,我下载了超过 20 个视频。

下载一些数据后,我们将创建一个名为bootstrap_videos.py的脚本。有关完整代码,请参考以下 GitLab 链接的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/bootstrap_videos.py

import os, json
import ipfsapi
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt

w3 = Web3(IPCProvider('/tmp/geth.ipc'))

common_password = 'bitcoin123'
accounts = []
with open('accounts.txt', 'w') as f:
...
...
    nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(account))
    txn = VideosSharing.functions.upload_video(ipfs_path, title).buildTransaction({
                'from': account,
                'gas': 200000,
                'gasPrice': w3.toWei('30', 'gwei'),
                'nonce': nonce
              })
    txn_hash = w3.personal.sendTransaction(txn, common_password)
    wait_for_transaction_receipt(w3, txn_hash)

让我们逐步详细讨论脚本。在导入必要的库之后,在以下代码块中,我们创建了一个名为w3的对象,它是连接到我们私有区块链的连接对象:

import os, json
import ipfsapi
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt

w3 = Web3(IPCProvider('/tmp/geth.ipc'))

在以下代码行中,我们使用w3.personal.newAccount()方法创建新账户。然后我们将新账户的地址放入accounts.txt文件和accounts变量中。所有账户都使用'bitcoin123'作为密码:

common_password = 'bitcoin123'
accounts = []
with open('accounts.txt', 'w') as f:
    for i in range(4):
        account = w3.personal.newAccount(common_password)
        accounts.append(account)
        f.write(account + "\n")

记住:在我们的私有区块链上部署智能合约后,我们将智能合约的地址保存在address.txt文件中。现在是时候将文件的内容加载到address变量中了:

with open('address.txt', 'r') as f:
    address = f.read().rstrip("\n")

with open('videos_sharing_smart_contract/build/contracts.json') as f:
    contract = json.load(f)
    abi = contract['VideosSharing']['abi']

然后,我们加载我们的智能合约的abi或接口,可以从 Populus 项目目录的build目录中的contracts.json中获取。我们使用json.load()方法将 JSON 加载到contract变量中。abi来自json对象的'VideosSharing'键中的'abi'键。

然后,我们使用w3.eth.contract()方法初始化智能合约对象的地址和接口。然后我们使用ipfsapi.connect()方法获取 IPFS 连接对象:

VideosSharing = w3.eth.contract(address=address, abi=abi)

c = ipfsapi.connect()

接下来,我们想要向我们的新账户转移以太币。默认情况下,第一个账户(w3.eth.accounts[0])获得所有来自挖矿的奖励,因此它有足够的以太币可以分享。默认密码是'this-is-not-a-secure-password'

coinbase = w3.eth.accounts[0]
coinbase_password = 'this-is-not-a-secure-password'
# Transfering Ethers
for destination in accounts:
    nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(coinbase))
    txn = {
            'from': coinbase,
            'to': Web3.toChecksumAddress(destination),
            'value': w3.toWei('100', 'ether'),
            'gas': 70000,
            'gasPrice': w3.toWei('1', 'gwei'),
            'nonce': nonce
          }
    txn_hash = w3.personal.sendTransaction(txn, coinbase_password)
    wait_for_transaction_receipt(w3, txn_hash)

通过w3.personal.sendTransaction()方法发送以太币,该方法接受包含发送者('from')、目的地('to')、以太币数量('value')、gas、燃气价格('gasPrice')、nonce作为第一个参数,密码作为第二个参数的字典。然后我们使用wait_for_transaction_receipt()方法等待交易确认。

在转移以太币之后,我们将我们的代币的一些 ERC20 硬币转移到新账户。这是必要的,因为要喜欢一个视频,我们需要我们的 ERC20 代币的硬币:

# Transfering Coins
for destination in accounts:
    nonce = w3.eth.getTransactionCount(coinbase)
    txn = VideosSharing.functions.transfer(destination, 100).buildTransaction({
                'from': coinbase,
                'gas': 70000,
                'gasPrice': w3.toWei('1', 'gwei'),
                'nonce': nonce
              })
    txn_hash = w3.personal.sendTransaction(txn, coinbase_password)
    wait_for_transaction_receipt(w3, txn_hash)

我们为转移代币方法(VideosSharing.functions.transfer)构建了一个交易对象txn,该方法接受目标帐户和硬币数量的buildTransaction方法。这个方法接受发送者('from')、燃气、燃气价格('gasPrice')和 nonce 的字典。我们使用w3.personal.sendTransaction()方法创建一个交易,然后使用wait_for_transaction_receipt()方法等待交易被确认。

我们使用os.listdir()方法列出stock_videos目录中的所有文件。您已经将一些 MP4 文件下载到此目录中。在这样做之后,我们遍历这些文件:

# Uploading Videos
directory = 'stock_videos'
movies = os.listdir(directory)
length_of_movies = len(movies)
for index, movie in enumerate(movies):
    account = accounts[index//7]
    ipfs_add = c.add(directory + '/' + movie)
    ipfs_path = ipfs_add['Hash'].encode('utf-8')
    title = movie.rstrip('.mp4')[:20].encode('utf-8')

    nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(account))
    txn = VideosSharing.functions.upload_video(ipfs_path, title).buildTransaction({
                'from': account,
                'gas': 200000,
                'gasPrice': w3.toWei('30', 'gwei'),
                'nonce': nonce
              })
    txn_hash = w3.personal.sendTransaction(txn, common_password)
    wait_for_transaction_receipt(w3, txn_hash)

我们希望每个帐户上传七个视频(account = accounts [index//7])。因此,前七个视频将由第一个帐户上传,而第二批七个视频将由第二个帐户上传。然后我们将 MP4 文件添加到 IPFS(ipfs_add = c.add(directory + '/' + movie))。我们获取 IPFS 路径并将其转换为字节对象(ipfs_path = ipfs_add['Hash'].encode('utf-8')),将 MP4 文件名削减为 20 个字符并将其转换为字节对象,因为智能合约中的标题具有bytes[20]数据类型。

然后我们调用我们智能合约的upload_video方法(VideosSharing.functions.upload_video)。在将其作为参数发送到w3.personal.sendTransaction()方法之前,我们必须构建交易对象。我们等待交易像往常一样被确认,使用wait_for_transaction_receipt()方法。

但是,您必须小心upload_video方法,因为它会保存视频路径,该路径具有bytes[50]数据类型,并且视频标题,该标题具有bytes[20]数据类型在区块链上。它还会增加视频的索引并记录事件。所需的燃气和燃气价格比转移硬币或代币方法要多得多。要转移代币硬币,您可以使用 1 gwei 和 70,000 gas 的燃气价格。但是,对于我们的upload_video方法,这将失败。对于此方法,我使用 30 gwei 和 200,000 gas 的燃气价格。请记住,在区块链中存储是昂贵的。即使一些字符串也可能会提高操作所需的燃气和燃气价格。

  1. 确保您已经启动了您的私有区块链,然后启动 IPFS daemon
$ ipfs daemon

如果您不知道如何安装和启动 IPFS,请参考第十一章,使用 ipfsapi 与 IPFS 交互

  1. 现在,我们需要在我们的虚拟环境中安装 IPFS Python 库:
(videos-venv) $ pip install ipfsapi
  1. 然后,我们使用以下命令运行我们的引导脚本:
(videos-venv) $ python bootstrap_videos.py

这将需要一些时间。您可以通过访问智能合约并检查视频是否已上传来测试您的引导脚本是否成功。

  1. 创建一个名为check_bootstrap.py的脚本:
import json
from web3 import Web3, IPCProvider

w3 = Web3(IPCProvider('/tmp/geth.ipc'))

with open('accounts.txt', 'r') as f:
    account = f.readline().rstrip("\n")

with open('address.txt', 'r') as f:
    address = f.read().rstrip("\n")

with open('videos_sharing_smart_contract/build/contracts.json') as f:
    contract = json.load(f)
    abi = contract['VideosSharing']['abi']

VideosSharing = w3.eth.contract(address=address, abi=abi)

print(VideosSharing.functions.latest_videos_index(account).call())
  1. 运行脚本。如果输出为0,则您的引导脚本失败。如果您得到除0之外的一些输出,则您的视频信息已成功上传到区块链中。

构建视频分享网络应用程序

现在是时候构建我们智能合约的前端了。在第七章和第九章中,我们使用 Qt for Python 或Pyside2库创建了一个桌面应用程序。这次我们将使用 Django 库构建一个 Web 应用程序:

  1. 话不多说,让我们安装 Django:
(videos-venv) $ pip install Django
  1. 我们还需要 OpenCV Python 库来获取我们视频的缩略图:
(videos-venv) $ pip install opencv-python
  1. 现在让我们创建我们的 Django 项目目录。这将创建一个带有其设置文件的骨架 Django 项目:
(videos-venv) $ django-admin startproject decentralized_videos
  1. 在这个新目录中,创建一个static media目录:
(videos-venv) $ cd decentralized_videos
(videos-venv) $ mkdir static media
  1. 在同一个目录中,创建一个名为videos的 Django 应用程序:
(videos-venv) $ python manage.py startapp videos
  1. 然后更新我们的 Django 项目设置文件。该文件位于decentralized_videos/settings.py。将我们的新应用程序videos添加到INSTALLED_APPS变量中。确保'videos''django.contrib.staticfiles'字符串之间有逗号。我们需要将每个 Django 应用程序添加到这个变量中,以便 Django 项目能够识别它。一个 Django 项目可以由许多 Django 应用程序组成:
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'videos'
]
  1. 然后,在同一个文件中,添加以下代码行:
STATIC_URL = '/static/'

STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "static"),
]

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

STATIC_URL变量定义了我们如何访问静态 URL。有了这个值,我们可以使用这个 URL 访问静态文件:http://localhost:8000/static/our_static_fileSTATICFILES_DIRS变量指的是我们在文件系统中保存静态文件的位置。我们简单地将视频存储在 Django 项目目录内的static目录中。MEDIA_URLSTATIC_URL的作用是一样的,但是用于媒体文件。媒体文件是用户上传到 Django 项目中的文件,而静态文件是我们作为开发者放入 Django 项目中的文件。

视图

现在让我们创建videos应用程序的视图文件。视图是一个类似 API 端点的控制器。该文件位于decentralized_videos/videos/views.py。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/views.py

from django.shortcuts import render, redirect
from videos.models import videos_sharing

def index(request):
    videos = videos_sharing.recent_videos()
    context = {'videos': videos}
    return render(request, 'videos/index.html', context)
...
...
def like(request):
    video_user = request.POST['video_user']
    index = int(request.POST['index'])
    password = request.POST['password']
    video_liker = request.POST['video_liker']
    videos_sharing.like_video(video_liker, password, video_user, index)
    return redirect('video', video_user=video_user, index=index)

让我们逐步讨论代码。首先,我们使用以下代码行导入所有所需的库:

from django.shortcuts import render, redirect
from videos.models import videos_sharing

renderredirect方法是 Django 库中的便利函数,用于渲染模板(如 HTML 文件)并将其从一个视图重定向到另一个视图。videos_sharing是我们将在models文件中很快创建的自定义实例。

接下来,我们将创建一个方法,这个方法将成为我们主页的视图:

def index(request):
    videos = videos_sharing.recent_videos()
    context = {'videos': videos}
    return render(request, 'videos/index.html', context)

我们从我们的模型实例中检索最近的视频。我们将构建这个类及其方法。我们使用包含videos对象的上下文渲染'videos/index.html'模板,稍后我们将创建这个模板。request参数是 POST 参数和 GET 参数的表示,还有其他内容。

然后,我们有以下代码行用于列出特定视频上传者的所有视频的页面:

def channel(request, video_user):
    videos = videos_sharing.get_videos(video_user)
    context = {'videos': videos, 'video_user': video_user}
    return render(request, 'videos/channel.html', context)

这个方法接受一个video_user参数,代表视频上传者的地址。我们从videos_sharing.get_videos方法中获取视频,该方法接受视频上传者的地址。然后我们使用包含视频和视频上传者地址的上下文渲染'videos/channel.html'模板文件。

在接下来的方法中,我们有一个用于播放视频的页面的视图:

def video(request, video_user, index):
    video = videos_sharing.get_video(video_user, index)
    context = {'video': video}
    return render(request, 'videos/video.html', context)

这个方法接受video_user参数,代表视频上传者的地址,以及index参数,代表视频的索引。我们从videos_sharing.get_video方法中获取特定视频,该方法接受video_userindex参数。接下来,我们使用包含这个视频的上下文渲染'videos/video.html'

然后,我们有一个视图,当我们上传视频文件、标题、视频上传者的地址和密码时调用:

def upload(request):
    context = {}
    if request.POST:
        video_user = request.POST['video_user']
        title = request.POST['title']
        video_file = request.FILES['video_file']
        password = request.POST['password']
        videos_sharing.upload_video(video_user, password, video_file, title)
        context['upload_success'] = True
    return render(request, 'videos/upload.html', context)

要检索 POST 参数,我们可以使用request.POST属性。然而,要访问我们正在上传的文件,我们使用request.FILES属性。这个视图用于上传文件的页面和处理文件本身。我们使用videos_sharing.upload_video方法将视频信息存储到区块链中。在这个方法的结尾,我们使用context渲染'videos/upload.html',如果我们成功上传了视频,就会包含一个成功的通知。

出于教育目的,我简化了上传代码,没有进行验证。此外,这个网络应用只被一个人使用。然而,如果你打算构建一个为许多陌生人提供服务的网络应用,你需要验证上传的文件。你还应该使用 Django 表单来处理 POST 参数,而不是手动处理。

接下来,在以下方法中,我们有喜欢视频的视图:

def like(request):
    video_user = request.POST['video_user']
    index = int(request.POST['index'])
    password = request.POST['password']
    video_liker = request.POST['video_liker']
    videos_sharing.like_video(video_liker, password, video_user, index)
    return redirect('video', video_user=video_user, index=index)

当我们想要喜欢一个视频时,我们会检索所有必要的信息,比如视频点赞者的地址、视频上传者的地址、视频的索引和密码,这样我们就可以获取特定的视频。然后我们使用videos_sharing.like_video方法来完成这项工作。点赞视频后,我们重定向到video视图。

模型

让我们在decentralized_videos/videos/models.py中创建我们的模型文件。大部分逻辑和繁重的操作都发生在这里。调用智能合约的方法和将文件存储到 IPFS 也发生在这里。请参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/models.py

import os.path, json
import ipfsapi
import cv2
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
from decentralized_videos.settings import STATICFILES_DIRS, STATIC_URL, BASE_DIR, MEDIA_ROOT

class VideosSharing:
...
...
        txn_hash = self.w3.personal.sendTransaction(txn, password)
        wait_for_transaction_receipt(self.w3, txn_hash)

videos_sharing = VideosSharing()

让我们逐步讨论我们 Django 项目的核心功能。首先,我们从 Python 标准库、IPFS Python 库、OpenCV Python 库、web3 库、Populus 库中导入方便的方法,以及从 Django 设置文件中导入一些变量:

import os.path, json
import ipfsapi
import cv2
from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
from decentralized_videos.settings import STATICFILES_DIRS, STATIC_URL, BASE_DIR, MEDIA_ROOT

然后,我们从VideosSharing模型的初始化代码开始:

class VideosSharing:

    def __init__(self):
        self.w3 = Web3(IPCProvider('/tmp/geth.ipc'))
        with open('../address.txt', 'r') as f:
            address = f.read().rstrip("\n")

        with open('../videos_sharing_smart_contract/build/contracts.json') as f:
            contract = json.load(f)
            abi = contract['VideosSharing']['abi']

        self.SmartContract = self.w3.eth.contract(address=address, abi=abi)

        self.ipfs_con = ipfsapi.connect()

我们通过创建一个 web3 连接对象w3来初始化这个实例,创建一个智能合约对象,提供智能合约的地址和接口SmartContract,最后创建一个 IPFS 连接对象ipfs_con

然后,我们有在index视图中使用的方法:

    def recent_videos(self, amount=20):
        events = self.SmartContract.events.UploadVideo.createFilter(fromBlock=0).get_all_entries()
        videos = []
        for event in events:
            video = {}
            video['user'] = event['args']['_user']
            video['index'] = event['args']['_index']
            video['path'] = self.get_video_path(video['user'], video['index'])
            video['title'] = self.get_video_title(video['user'], video['index'])
            video['thumbnail'] = self.get_video_thumbnail(video['path'])
            videos.append(video)
        videos.reverse()
        return videos[:amount]

最近的视频可以从事件中检索。如果你还记得我们在智能合约中上传视频时,会记得我们在这里记录了一个事件。我们的事件是UploadVideo。因为这个 Django 项目只是一个玩具应用,我们从起始块获取所有事件。在现实世界中,你可能希望限制它(也许是最后 100 个块)。此外,你可能希望在后台作业(比如 cron)中将事件存储到数据库中,以便轻松检索。这个事件对象包含了视频上传者和视频的索引。根据这些信息,我们可以获取视频路径、视频标题和视频缩略图。我们在videos对象中累积视频,然后将其反转(因为我们想获取最近的视频),并将这个对象返回给方法的调用者。

然后,我们有一个从特定视频上传者那里获取视频的方法:

    def get_videos(self, user, amount=20):
        latest_index = self.SmartContract.functions.latest_videos_index(user).call()
        i = 0
        videos = []
        while i < amount and i < latest_index:
            video = {}
            index = latest_index - i - 1
            video['user'] = user
            video['index'] = index
            video['path'] = self.get_video_path(user, index)
            video['title'] = self.get_video_title(user, index)
            video['thumbnail'] = self.get_video_thumbnail(video['path'])
            videos.append(video)
            i += 1
        return videos

这在channel视图中使用。首先,我们获取这个视频上传者的最新视频索引。根据这些信息,我们可以找出视频上传者上传了多少个视频。然后,我们从最高索引到最低索引逐个检索视频,直到视频数量达到我们需要的数量。

这些是根据视频上传者的地址获取视频路径和视频标题的方法:


    def get_video_path(self, user, index):
        return self.SmartContract.functions.videos_path(user, index).call().decode('utf-8')

    def get_video_title(self, user, index):
        return self.SmartContract.functions.videos_title(user, index).call().decode('utf-8')

视频索引定义如下:

    def process_thumbnail(self, ipfs_path):
        thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
        if not os.path.isfile(thumbnail_file):
            video_path = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
            cap = cv2.VideoCapture(video_path)
            cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
            _, frame = cap.read()
            cv2.imwrite(thumbnail_file, frame)

我们使用了我们智能合约的videos_pathvideos_title方法。不要忘记解码结果,因为bytes对象形成了我们的智能合约。

以下代码块是获取视频缩略图的方法:

    def get_video_thumbnail(self, ipfs_path):
        thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
        url_file = STATIC_URL + '/' + ipfs_path + '.png'
        if os.path.isfile(thumbnail_file):
            return url_file
        else:
            return "https://bulma.io/images/placeholders/640x480.png"

当我们在播放视频页面查看视频时,我们会检查是否有一个特定的带有.png文件扩展名的文件名。我们在static files目录中找到这个文件名模式。如果找不到文件,我们就从互联网上使用一个占位图片文件。

以下代码块是检索特定视频的方法:

    def get_video(self, user, index):
        video = {}
        ipfs_path = self.get_video_path(user, index)
        video_title = self.get_video_title(user, index)
        video_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
        thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
        video['title'] = video_title
        video['user'] = user
        video['index'] = index
        video['aggregate_likes'] = self.SmartContract.functions.video_aggregate_likes(user, index).call()

        if os.path.isfile(video_file):
            video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'
        else:
            self.ipfs_con.get(ipfs_path)
            os.rename(BASE_DIR + '/' + ipfs_path, STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4')
            video['url'] = STATIC_URL + '/' + ipfs_path + '.mp4'

        if not os.path.isfile(thumbnail_file):
            self.process_thumbnail(ipfs_path)

        return video

这在video视图中使用。我们需要视频路径、视频标题、视频文件、视频缩略图和这个视频的总点赞数(我们可以使用智能合约的video_aggregate_likes方法获取)。我们检查静态文件目录中是否存在这个 MP4 文件。如果不存在,我们使用ipfs_con.get方法从 IPFS 中检索它。然后我们将文件移动到静态文件目录,并在尚未存在时创建缩略图图像。

在现实世界中,您可能希望使用 Celery 和 RabbitMQ 等后台作业来从 IPFS 中检索文件。对于这个玩具应用程序,我们只是以阻塞的方式下载视频。但是,安装和配置 Celery 和 RabbitMQ 并不是一件轻松的事情,我认为这会分散我们在这里的教育目的。

以下方法演示了当我们上传视频时会发生什么:

    def upload_video(self, video_user, password, video_file, title):
        video_path = MEDIA_ROOT + '/video.mp4'
        with open(video_path, 'wb+') as destination:
            for chunk in video_file.chunks():
                destination.write(chunk)
        ipfs_add = self.ipfs_con.add(video_path)
        ipfs_path = ipfs_add['Hash'].encode('utf-8')
        title = title[:20].encode('utf-8')
        nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_user))
        txn = self.SmartContract.functions.upload_video(ipfs_path, title).buildTransaction({
                    'from': video_user,
                    'gas': 200000,
                    'gasPrice': self.w3.toWei('30', 'gwei'),
                    'nonce': nonce
                  })
        txn_hash = self.w3.personal.sendTransaction(txn, password)
        wait_for_transaction_receipt(self.w3, txn_hash)

我们将文件保存在内存中的媒体目录中,然后使用ipfs_con.add方法将文件添加到 IPFS。我们获取 IPFS 路径并准备视频的标题。然后,我们从智能合约中调用upload_video方法。记得为此设置足够的 gas 和 gas 价格。这是一个非常昂贵的智能合约方法。我们等待交易确认。在现实世界中,您可能希望使用后台作业来执行所有这些步骤。

以下代码块显示了如何从视频生成缩略图:

    def process_thumbnail(self, ipfs_path):
        thumbnail_file = STATICFILES_DIRS[0] + '/' + ipfs_path + '.png'
        if not os.path.isfile(thumbnail_file):
            video_path = STATICFILES_DIRS[0] + '/' + ipfs_path + '.mp4'
            cap = cv2.VideoCapture(video_path)
            cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
            _, frame = cap.read()
            cv2.imwrite(thumbnail_file, frame)

确保不存在这样的文件后,我们获取视频对象。我们读取对象的第一帧并将其保存到图像文件中。这个视频功能来自 OpenCV Python 库。

然后,我们有点赞视频的方法:

    def like_video(self, video_liker, password, video_user, index):
        if self.SmartContract.functions.video_has_been_liked(video_liker, video_user, index).call():
            return
        nonce = self.w3.eth.getTransactionCount(Web3.toChecksumAddress(video_liker))
        txn = self.SmartContract.functions.like_video(video_user, index).buildTransaction({
                    'from': video_liker,
                    'gas': 200000,
                    'gasPrice': self.w3.toWei('30', 'gwei'),
                    'nonce': nonce
                  })
        txn_hash = self.w3.personal.sendTransaction(txn, password)
        wait_for_transaction_receipt(self.w3, txn_hash)

我们通过调用智能合约的video_has_been_liked方法来确保这个视频还没有被点赞。然后我们使用智能合约的like_video方法来点赞视频。

最后,我们创建了VideosSharing类的一个实例,以便可以导入这个实例:

videos_sharing = VideosSharing()

我更喜欢导入一个类的实例,而不是导入一个类。因此,我们在这里初始化一个类的实例。

模板

是时候写我们的模板了。首先,让我们使用以下命令行创建一个模板目录:

(videos-venv) $ cd decentralized_videos
(videos-venv) $ mkdir -p videos/templates/videos

然后,我们首先使用以下 HTML 代码创建我们的基本布局。这是所有我们模板将使用的布局。文件位于videos/templates/videos/base.html。您可以参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/templates/videos/base.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Decentralized Videos Sharing Application</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
...
...
    </section>
    {% block content %}
    {% endblock %}
  </body>
</html>

在页眉中,我们导入 Bulma CSS 框架和 Font Awesome JavaScript 文件。在这个基本布局中,我们设置了我们的导航,其中包含首页链接和视频上传链接。在{% block content %}{% endblock %}之间的部分将由我们模板的内容填充。

虽然本书专注于教授 Python,尽量避免其他技术,如 CSS 和 JavaScript,但一些 CSS 是必要的,以使我们的 Web 应用程序看起来体面。您可以访问bulma.io了解这个 CSS 框架。

然后,让我们在videos/templates/videos/index.html中创建我们的第一个模板文件。使用以下代码块创建模板文件:

{% extends "videos/base.html" %}

{% block content %}
<section class="section">
  <div class="container">
    {% for video in videos %}
      {% cycle '<div class="columns">' '' '' '' %}
        <div class="column">
          <div class="card">
            <div class="card-image">
              <figure class="image is-4by3">
                <img src="{{ video.thumbnail }}" />
              </figure>
            </div>
            <p class="card-footer-item">
              <span><a href="{% url 'video' video_user=video.user index=video.index %}">{{ video.title }}</a></span>
            </p>
          </div>
        </div>
      {% cycle '' '' '' '</div>' %}
    {% endfor %}
  </div>
</section>
{% endblock %}

首先,我们确保这个模板扩展了我们的基本布局。然后我们在模板中显示我们的视频。我们使用card类 div 来显示视频。cycle方法用于生成columns类 div 来包含四个column类 div。第二个cycle方法用于关闭这个 div。在这个card的页脚,我们创建了一个链接到播放此视频的页面。url方法接受 URL 名称(我们将很快讨论)及其参数。

然后,我们将创建一个模板文件,在videos/templates/videos/video.html中播放视频。您可以参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/templates/videos/video.html

{% extends "videos/base.html" %}

{% block content %}
<section class="section">
  <div class="container">
    <nav class="breadcrumb" aria-label="breadcrumbs">
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/channel/{{ video.user }}">Channel</a></li>
        <li class="is-active"><a href="#" aria-current="page">{{ video.title }}</a></li>
      </ul>
    </nav>

...
...

  </div>
</section>
{% endblock %}

在扩展基本布局之后,我们创建一个breadcrumb,以便用户可以转到视频上传者的频道页面。然后我们使用video HTML 标签显示视频。在视频下方,我们显示聚合点赞数。在页面底部,我们创建一个表单来点赞视频。这个表单接受视频点赞者的地址和用户输入的密码。有隐藏的输入来发送视频上传者的地址和视频索引。请注意,这个表单内有一个名为{% csrf_token %}的 CSRF 令牌。这是为了避免 CSRF 漏洞。

然后让我们创建一个模板文件,用于在videos/templates/videos/channel.html中列出特定视频上传者的所有视频。您可以参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/templates/videos/channel.html

{% extends "videos/base.html" %}

{% block content %}
<section class="section">
  <div class="container">
    <nav class="breadcrumb" aria-label="breadcrumbs">
      <ul>
        <li><a href="/">Home</a></li>
        <li class="is-active"><a href="#">{{ video_user }}</a>
...
...
            </p>
          </div>
        </div>
      {% cycle '' '' '' '</div>' %}
    {% endfor %}
  </div>
</section>
{% endblock %}

这个模板文件与索引模板相同,只是在视频列表的顶部有一个breadcrumb

让我们创建最后一个模板文件,用于在videos/templates/videos/upload.html中上传视频。您可以参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_12/decentralized_videos/videos/templates/videos/upload.html

{% extends "videos/base.html" %}

{% block content %}
<section class="section">
  <div class="container">
    <nav class="breadcrumb" aria-label="breadcrumbs">
      <ul>
        <li><a href="/">Home</a></li>
        <li class="is-active"><a href="#" aria-current="page">Uploading Video</a></li>
      </ul>
    </nav>
    <div class="content">
...
...
</section>
<script type="text/javascript">
var file = document.getElementById("video_file");
file.onchange = function() {
  if(file.files.length > 0) {
    document.getElementById('video_filename').innerHTML = file.files[0].name;
  }
};
</script>
{% endblock %}

在这个模板中,扩展基本布局后,我们创建了breadcrumb。然后,我们创建一个上传视频的表单。

这里有四个输入——视频标题、视频文件、视频上传者的地址和密码。模板底部的 JavaScript 代码用于在选择文件后将文件名设置为文件上传字段的标签。因为我们正在上传文件,所以需要将表单的enctype属性设置为"multipart/form-data"

Urls

urls文件是 Django 中的路由机制。打开decentralized_videos/videos/urls.py,删除内容,并用以下脚本替换:

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('channel/<str:video_user>', views.channel, name='channel'),
    path('video/<str:video_user>/<int:index>', views.video, name='video'),
    path('upload-video', views.upload, name='upload'),
    path('like-video', views.like, name='like'),
]

还记得我们之前创建的视图文件吗?在这里,我们将视图映射到路由。我们通过http://localhost:8000/video/0x0000000000000000000000000000000000000000/1访问视频播放页面。参数将映射到video_user变量和index变量。path方法的第一个参数是我们在浏览器中调用它的方式。第二个方法是我们使用的视图,第三个参数是在模板中使用的路由名称。

然后我们需要将这些urls注册到项目urls文件中。编辑decentralized_videos/decentralized_videos/urls.py并添加我们的videos.urls路径,以便我们的 Web 应用知道如何将我们的 URL 路由到我们的videos视图:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('', include('videos.urls')),
    path('admin/', admin.site.urls)
]

演示

现在是享受劳动成果的时候了。在运行服务器之前,请确保您在decentralized_videos目录中。不要忘记先运行私有区块链和 IPFS 守护程序:

(videos-venv) $ cd decentralized_videos
(videos-venv) $ python manage.py runserver

然后打开http://localhost:8000。在这里,您将看到最近的视频,如下图所示。如果您对为什么有一些视频的缩略图感到困惑,您需要转到播放视频的页面生成缩略图:

让我们点击其中一个视频:

您可以在这里播放视频。

要在 Web 上播放 HTML5 视频,我们可以使用 Chrome 浏览器。您也可以使用 Firefox 浏览器,但是您需要采取额外的步骤来启用浏览器上的视频播放,方法是按照以下网站上的步骤进行操作:stackoverflow.com/questions/40760864/how-to-play-mp4-video-in-firefox

您还可以使用表单喜欢视频。让我们点击面包屑中的“频道”链接:

这是特定视频上传者的视频列表。最后,让我们转到上传视频页面。点击导航菜单中的“上传”链接:

您可以在这里上传视频,如上图所示。

注意

为了使这个应用在现实世界中表现更好,有很多事情需要做。您需要添加测试,测试模型、视图、模板,最后,您需要进行完整的集成测试。您还需要使用 Celery 和 RabbitMQ 或 Redis 将繁重和长时间的操作(如在智能合约上调用操作,使用 IPFS 添加和获取文件)放在后台作业中。除此之外,您还需要添加一些 JavaScript 文件,以便使用轮询机制通知后台作业是否已经完成。您还可以使用 Django 通道来完成这项工作。

而不是在模型中访问智能合约的方法,也许最好是使用 cron 在后台任务中将区块链中的所有信息存储在数据库中。然后模型可以访问数据库以获取必要的信息。要上传和喜欢视频,我们需要每次发送我们的地址和密码。也许,为了方便起见,我们可以为用户提供一种临时保存地址和密码的方法。我们可以将这些保存在会话中、cookie 中,甚至是 web3 对象中。

在我们的玩具应用中,我们假设每个人都上传了有效的视频文件。如果有人上传了无效的视频文件,我们需要处理这种情况。此外,如果有人上传了无效的视频的 IPFS 路径,也需要相应处理。我们应该在智能合约中验证(使用更多的 gas)吗?还是应该在前端验证?我们需要处理许多边缘情况。我们还需要添加分页。搜索呢?我们需要爬取区块链上的事件。我们只关心视频标题,还是应该从视频文件本身提取信息?这些都是您需要考虑的问题,如果您想在现实世界中构建一个去中心化的视频分享应用。

摘要

在本章中,我们结合了 IPFS 技术和智能合约技术。我们构建了一个去中心化的视频分享应用程序。首先,我们编写了一个智能合约来存储视频信息和视频标题。我们还通过让点赞视频需要使用 ERC20 代币来内置加密经济学。除此之外,我们还了解到,即使存储视频信息,比如 IPFS 路径和标题的字节字符串,也需要比平常更多的 gas。在编写智能合约之后,我们使用 Django 库构建了一个 Web 应用程序。我们创建了一个项目,然后在这个项目内构建了一个应用程序。接着,我们构建了视图、模型、模板和 URL。在模型中,我们将视频文件存储在 IPFS 上,然后将 IPFS 路径存储在区块链上。我们使用 Bulma CSS 框架使模板更加美观,然后通过执行这个 Web 应用程序的功能来启动应用程序。

在本书中,我们学习了区块链是什么,以及智能合约是什么。我们使用 Vyper 编程语言构建了许多有趣的智能合约,比如投票智能合约、类似 Twitter 的应用程序智能合约、ERC20 代币智能合约和视频分享智能合约。我们还利用 web3 库与这些智能合约进行交互,并构建了去中心化应用程序。除此之外,我们使用 PySide2 库构建了我们的 GUI 前端,用于处理以太和 ERC20 代币的加密货币钱包应用程序。最后,我们还学习了一种补充的去中心化技术 IPFS,它可以成为区块链应用程序的存储解决方案。

掌握了所有这些技能之后,您就有能力在以太坊平台上构建许多有趣的应用程序。但以太坊仍然是一项新兴技术。诸如分片、权益证明和隐私等技术仍在以太坊中进行研究和开发。这些新技术可能会影响您所学到的技术,比如 Vyper 和 web3。因此,您需要关注以太坊平台的新更新。

进一步阅读