本文转载自微信公众号「区块链研究实验室」,作者链三丰。转载本文请联系区块链研究实验室公众号。
区块链不仅是一个流行词。它也不限于加密货币和比特币。凭借其创造透明度和公平性的能力,这项技术正在革新各个领域。应用范围从跟踪系统到保护数据,再到执行在线投票系统。它可以帮助实施反洗钱跟踪系统,或者简单地跟踪您在商店购买的产品的来源。
就像信息科学中经常发生的那样,许多区块链平台管理着所有的复杂性,使我们可以像保存一个简单的数据库一样简单地保存数据。
在本文中,我想实现一个区块链数据库,以了解此类解决方案的关键要素。而且,为了使其更具挑战性,我将在不使用任何数据库或服务器的情况下做到这一点。
该解决方案可以轻松地使您拥有可以验证并安全存储的不可变数据。
这篇文章的结构如下:
- 什么是区块链数据库及其使用方式
- 如何仅使用DNS服务来实现区块链
什么是区块链数据库以及如何使用
像往常一样,我们可以从Wikipedia定义开始:
“A blockchain,[ ...],是一个越来越多的记录,称为块,正在使用链接加密。每个块都包含前一个块的加密哈希,即时间戳[..]。通过设计,区块链可以抵抗其数据的修改。这是因为一旦记录,任何给定块中的数据都不能追溯更改,而无需更改所有后续块。
“对于用作分布式总账,一个blockchain通常由管理对等网络的网络共同地粘附到协议用于节点间通信和验证新块”。
换句话说,区块链的主要特征是:
- 通过将一条记录连接到上一条记录来存储数据
- 做到这一点,因此您不能在不使所有数据顺序不一致的情况下更改一条记录
- 将数据存储在分布式数据库中
那么,如何创建呢?
我想的是,一个节点的链或多或少是一个链表,其中每个块都有一个不可变的哈希。完成此操作后,您只需要一个安全的分布式数据库即可存储数据。什么是古老的分布式数据库?好吧,每个人都有一个分布式数据库,没人知道!我说的是DNS。是的,它是分布式的,它存储数据。每个人都有一个DNS服务。我意识到这不是预期的用途,但让我们一起玩吧。
该协议的工作流程是受信任的机构将数据写入DNS。每个记录都有一个唯一的键,该键是内容的哈希值。这意味着,通过更改数据,您将更改ID,并且指向该ID的所有子代都将不一致。此外,DNS协议是分布式的,因此许多服务器之间共享数据的许多副本,这意味着您的一个DNS将脱机,而另一个将继续为数据提供服务。还请考虑DNS被广泛缓存,这使您的通信性能高(使用不可变数据缓存永远不会成为问题)。
该系统使用所有公司都已经拥有的DNS作为存储,因此无需任何额外费用。DNS本身是一个分布式数据库。
现在我们已经定义了存储数据的位置,我们只需要了解如何存储数据即可。下一步是定义一个通信协议,使所有各方都可以扮演自己的角色。下图显示了流程。
DNS区块链工作流程。
在上图中,我们有:
- 在DNS上发布的推力实体。它是写作的关键-其他人可以写记录,但是它们是无法理解的。
- 一个消费者,即推力生产者和读取数据
- 数据,其 可以是任何JSON数据。您可以选择将其公开或不公开。
如何实施
现在我们知道该怎么做,并且已经有了启动该工具的工具,我们只需要使用源代码即可。
为了使用DNS实现区块链,我们必须面对一些重要问题:
- DNS限制-DNS并非旨在存储大数据。我们想使用TXT记录,但是它们只有254个字符。如果我们要存储一个大的JSON对象,这是一个很大的限制。
- 安全性-即使我们想保持数据公开,DNS使用的UDP协议也存在问题。它没有经过加密,并且没有像HTTPS协议中那样可以推动授权的证书机制。
- 数据是按设计公开的—这可能是一个问题。
所有这些方面都有一个解决方案,并且您将看到,它很容易实现。实际上,通过使用加密技术和独创性,我们将为上述所有问题找到一个明智的解决方案。
让我们看看它是如何工作的。
创建沙盒环境
第一步是创建一个我们想玩的沙盒环境。我们需要启动该工作的是带有API系统的本地DNS服务器。我们通过创建一个托管该文件的docker-compose文件来实现这一目标。我使用了一个Visual Studio项目,在其中创建了一个我们将用于验证数据的Web应用程序,一个将成为我们核心的库以及一个测试项目。结果如下:
DNS区块链项目
通过运行docker-compose up,所有启动并准备好进行测试。对于DNS部分,我使用了非常轻巧且具有HTTP API可用的DNS。它使用以下配置运行:
- version: '3.4'
- services:
- blockchaindns.web:
- image: ${DOCKER_REGISTRY-}blockchaindnsweb
- build:
- context: .
- dockerfile: BlockChainDNS.Web/Dockerfile
- dns:
- image: tgpfeiffer/shaman-dns
- command: shaman --server --token xxx --api-listen 0.0.0.0:1632 --dns-listen 0.0.0.0:53 -l trace --insecure
- ports:
- - 1632:1632
- - 53:53/udp
这里xxx是您要用于身份验证的令牌,并且DNS已配置为接受来自所有主机的请求(0.0.0.0:port)。
使用运行它之后docker-compose up,您可以使用控制台对其进行测试:
- #create a record in Shaman DNS
- curl --location --request POST 'localhost:1632/records' \
- --header 'Content-Type: application/json' \
- --data-raw '{
- "domain": "test.myfakedomain.it.",
- "records": [
- {
- "ttl": 60,
- "class": "IN",
- "type": "A",
- "address": "127.0.0.1"
- }
- ]
- }'
- #test the record
- nslookup test.myfakedomain.it 127.0.0.1
- #output
- # Server: UnKnown
- # Address: 127.0.0.1
- # Response from server:
- # Nome: test.myfakedomain.it
- # Address: 127.0.0.1
现在我们有了一个可以正常工作的本地DNS,我们可以创建一个可以通过API管理DNS记录的客户端。
DNS客户端
第二步是包装要在应用程序中使用的DNS客户端功能。我想在这里做的是将来有能力更改DNS服务,因此我创建了一个接口和一个类实现。以下代码片段显示了该界面:
- public interface IDNSClient
- {
- Task<DNSEntry> GetRecord(string host, string zone);
- Task<bool> AddRecord(DNSEntry entry);
- Action Init { get; set; }
- }
如您所见,客户端实现执行HTTP调用来存储记录。您可以在本文末尾的GitHub项目中找到完整的类实现。我们仅为萨满实施了本地提供商,但很容易对其进行扩展以支持大多数现代托管提供商上的任何商业DNS。
区块链服务
现在我们已经有了执行部分,是时候实现业务逻辑了。所有工作都在客户端完成,客户端将计算要存储的数据并调用DNS客户端方法以保留记录。服务层由两部分组成:
- BlockChainNode:节点的表示形式
- BlockChainService:实现逻辑的服务
让我们详细了解这些类如何工作。
区块链节点
这是带有JObject属性的简单类,用户可以在其中存储任何数据。它计算密钥哈希数据。数据包含历史记录,该历史记录是到父项的链接。仅更改数据的字节将更改密钥,这将导致以下节点不一致。以下代码显示了该类最重要的部分。
- public class BlockChainNode
- {
- public BlockChainNode()
- {
- this.History.CollectionChanged += History_CollectionChanged;
- }
- private void History_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
- {
- this.Data["_history"] = JArray.FromObject(this.History);
- }
- private JObject _data = new JObject();
- public JObject Data
- {
- get { return _data; }
- set { _data = value; History_CollectionChanged(null, null); }
- }
- public string Hash { get
- {
- return GetHash(this.ToBase64());
- }
- }
- public ObservableCollection<string> History { get; set; } = new ObservableCollection<string>();//First to last
- public string ToBase64()
- {
- var content = UnicodeEncoding.Unicode.GetBytes(Data.ToString(Formatting.None));
- return Convert.ToBase64String(content);
- }
- public static string GetHash(string text)
- {
- using (var md5 = MD5.Create())
- {
- return Base32.ToBase32String(md5.ComputeHash(UTF8Encoding.UTF8.GetBytes(text))).ToLower();
- }
- }
- }
该代码最相关的部分是:
- 数据对象:用户可以在其中存储数据的JSON对象
- 历史记录:与数据同步的可观察列表(“历史记录”中的任何更改都会更改_history节点,反之亦然。)
- 哈希:根据数据的文本表示形式的MD5计算得出的哈希。结果以Base32算法编码-类似于Base 64,但仅使用四个字节且仅包含小写字符。这是因为DNS不区分大小写,并且使用广泛使用的Base64编码产生了不一致的数据。
现在我们有了模型,我们必须继续下一步:由服务实现的业务逻辑。
区块链服务
区块链服务实现用于保存,读取和验证记录的方法。困难的部分是要解决DNS服务器记录长度的255个字符的限制。解决方案是在Base64中对内容进行编码,然后使用命名约定将其拆分成块保存在不同的记录中。密钥用作URL的一部分。因此,对于该项目mykey.domain.dom,我们将有0.mykey.domain.dom,1.mykey.domain.dom等下一段代码显示了节能方法。
- private int WriteDNSFragmentedText(string baseUrl, string value, int size)
- {
- var tokens = Tokenize(value, size).ToList();
- int i = 0;
- foreach (var token in tokens)
- {
- WriteDNSRecord($"{i}.{baseUrl}", "TXT", token);
- i++;
- }
- return i ;
- }
- private void WriteDNSRecord(string domain, string type, string value)
- {
- this.client.AddRecord(new DNSEntry()
- {
- Domain = domain,
- Type = type,
- Value = value
- });
- }
从上一个调用的片段中可以看到WriteDNSFragmentedText,输入文本被拆分,数据被保存在许多DNS条目中。
读取数据是相反的。我尝试获取子记录0,1,2,依此类推,直到有数据为止。一旦我收集了所有Base64块,过程就是将它们连接,解码并获取纯JSON。
- private string ReadDNSFragmentedText(string domain)
- {
- List<string> fragments = new List<string>();
- for (int i = 0; i < 1000; i++)
- {
- var fragmentUrl = $"{i}.{domain}";
- var result = ReadDNSTxtResult(fragmentUrl);
- if (result == null) break;// otherwise parent domain value will be added
- fragments.Add(result);
- }
- return string.Join("", fragments);
- }
- private string ReadDNSTxtResult(string fragmentUrl)
- {
- if (!fragmentUrl.EndsWith("."))
- {
- fragmentUrl = fragmentUrl + ".";
- }
- var result = lookup.QueryAsync(fragmentUrl, QueryType.TXT).Result;
- if (result != null && !result.HasError && result.Answers?.Count > 0 )
- {
- var resultDomain = result.Answers.FirstOrDefault().DomainName.Value;
- if (resultDomain == fragmentUrl)
- {
- return result.Answers.TxtRecords().FirstOrDefault()?.EscapedText.FirstOrDefault();
- }
- }
- return null;
- }
客户端可以轻松地验证所获取的数据是否生成密钥并且是否有效,因为客户端可以获取数据,哈希并比较结果。此外,客户端可以递归验证以检查所有父节点是否都是真实的。这就是验证过程所要做的。它由下一部分代码表示:
- public List<string> Validate(JObject data, string key, int db, string domain, byte[] privateKey, string expectedKey = null)
- {
- var errors = new List<string>();
- //ValidateBase: Coherence betweeen data and values.
- var computed = this.Get(key, db, domain, privateKey);
- if (key != computed.Hash)
- {
- errors.Add("Key mismatch");
- }
- ValidateHierarchy(key,db,domain,privateKey, ref errors);
- return errors;
- }
- private List<string> ValidateHierarchy(string key, int db, string domain, byte[] privateKey, ref List<string> errors)
- {
- var computed = this.Get(key, db, domain, privateKey);
- if (computed == null) return new List<string>();
- if (computed.History.Count > 0)
- {
- var hierarchy = ValidateHierarchy(computed.History.Last(), db, domain, privateKey, ref errors);
- if (hierarchy.Count != computed.History.Count-1)
- {
- errors.Add($"{computed.Hash}: history count not match with lookup");
- }
- else
- {
- for (int i = 0; i< hierarchy.Count; i++)
- {
- if (hierarchy[i] != computed.History[i])
- {
- errors.Add($"{computed.Hash}: history do not match at {computed.History[i]}");
- }
- }
- }
- }
- return computed.History.ToList();
- }
如您在上一个片段中看到的那样,将验证记录,然后下载所有层次结构并检查数据一致性。
现在我们了解了如何从DNS写入和读取数据,下一步是如何确保它们的安全。
密码学和密钥
我们的系统可以向DNS读取和写入数据,现在该注意安全了。我们假设写给我们的DNS的人是受信任的,但是我们不能确保恶意的DNS服务器不会给我们伪造数据或有人不会读取它(请记住,DNS数据是公共的)。
我在这里所做的就是对协议进行了以下改进:使用非对称算法对存储的数据进行加密存储。这样可以确保只有数据生产者才能生成消费者可以理解的数据。任何人都可以创建伪造的DNS服务器,但是他们将无法对待您伪造数据。而且,数据现在已加密,没有人可以读取。
非对称算法是完美的,因为它只允许一定数量的读者理解消息,但是只有消息源才能产生消息。为此,客户端生成一对密钥。公钥用于加密数据,因此生产者可以安全地保护它。与使用者共享用于解密的私钥。可以手动共享它,例如通过电子邮件将其发送到加密的存档中,或者发布在HTTPS网站上,证书可以在该网站上向用户展示权限。
顺便说一下,这个概念很简单:现在数据已加密,没有人可以代表我们写入数据。但是还有另一个问题。对称算法只处理少量数据(1024-4096字节),但我们必须处理巨大的JSON有效负载。我们有两种方法:
- 将完整的消息分成小字节块,并一一加密/解密它们。
- 创建一个对称密钥,使用生成的密钥对数据加密,然后使用非对称对对生成的密钥进行加密。这样,每个记录都具有用于加密数据的不同对称密钥。该密钥是公开共享的,但只有拥有私有密钥的人才能使用。
考虑到对所有字节块进行编码的计算量,我使用了第二种解决方案。这将我们带到下一个有效负载:
- {
- "data":"json object encrypted with the symm key",
- "key":"symm key encripted with the aymm alghorithm"
- }
在上面的代码段中,我们可以看到存储在JSON有效负载中的加密数据和解密密钥。读取器将使用私钥解密对称密钥,然后将其用于解密数据。
代码中的更改是最小的:所需的只是包装\展开数据的附加步骤。
在下一个代码段中,我显示了完成数据生成的步骤:
- #generate a one time password
- var password = SHA512.Create().ComputeHash(Guid.NewGuid().ToByteArray());
- #encrypt the password
- var decriptkey = this.cryptoService.EncodeKey(password, publicKey);
- #encrypt data with the password
- var dataEnrypted = this.cryptoService.EncryptData(dataToEncode, password);
- #json object is stored with decriptkey and dataEnrypted
在下一个代码段中,我们具有阅读过程:
- var decriptKeyEncoded = .. from json
- var dataEncrypted = ... from json
- var decriptKey = this.cryptoService.DecodeKey(decriptKeyEncoded, privateKey);
- var decodedData = this.cryptoService.DecodeData(dataEncrypted, decriptKey);
- #decodedData is the plain data.
既然我们已经完成了有关区块链实现的说明,那么我们就拥有了将数据存储在DNS区块链中的所有详细信息。
结论
即使将DNS服务器用作数据库看起来似乎很聪明,但事实并非如此。DNS并非旨在以这种方式存储数据。
如果我们必须处理安全的不可变数据,则解决方案是使用标准的区块链平台,我的意思是需要使用一个真正的区块链系统。
无论如何,尝试实现无服务器的区块链非常有趣,我希望它教会了我们区块链平台背后的原理。