如何使用DNS構建無成本的區(qū)塊鏈數(shù)據(jù)庫
本文轉載自微信公眾號「區(qū)塊鏈研究實驗室」,作者鏈三豐。轉載本文請聯(lián)系區(qū)塊鏈研究實驗室公眾號。
區(qū)塊鏈不僅是一個流行詞。它也不限于加密貨幣和比特幣。憑借其創(chuàng)造透明度和公平性的能力,這項技術正在革新各個領域。應用范圍從跟蹤系統(tǒng)到保護數(shù)據(jù),再到執(zhí)行在線投票系統(tǒng)。它可以幫助實施反洗錢跟蹤系統(tǒng),或者簡單地跟蹤您在商店購買的產(chǎn)品的來源。
就像信息科學中經(jīng)常發(fā)生的那樣,許多區(qū)塊鏈平臺管理著所有的復雜性,使我們可以像保存一個簡單的數(shù)據(jù)庫一樣簡單地保存數(shù)據(jù)。
在本文中,我想實現(xiàn)一個區(qū)塊鏈數(shù)據(jù)庫,以了解此類解決方案的關鍵要素。而且,為了使其更具挑戰(zhàn)性,我將在不使用任何數(shù)據(jù)庫或服務器的情況下做到這一點。
該解決方案可以輕松地使您擁有可以驗證并安全存儲的不可變數(shù)據(jù)。
這篇文章的結構如下:
- 什么是區(qū)塊鏈數(shù)據(jù)庫及其使用方式
- 如何僅使用DNS服務來實現(xiàn)區(qū)塊鏈
什么是區(qū)塊鏈數(shù)據(jù)庫以及如何使用
像往常一樣,我們可以從Wikipedia定義開始:
“A blockchain,[ ...],是一個越來越多的記錄,稱為塊,正在使用鏈接加密。每個塊都包含前一個塊的加密哈希,即時間戳[..]。通過設計,區(qū)塊鏈可以抵抗其數(shù)據(jù)的修改。這是因為一旦記錄,任何給定塊中的數(shù)據(jù)都不能追溯更改,而無需更改所有后續(xù)塊。
“對于用作分布式總賬,一個blockchain通常由管理對等網(wǎng)絡的網(wǎng)絡共同地粘附到協(xié)議用于節(jié)點間通信和驗證新塊”。
換句話說,區(qū)塊鏈的主要特征是:
- 通過將一條記錄連接到上一條記錄來存儲數(shù)據(jù)
- 做到這一點,因此您不能在不使所有數(shù)據(jù)順序不一致的情況下更改一條記錄
- 將數(shù)據(jù)存儲在分布式數(shù)據(jù)庫中
那么,如何創(chuàng)建呢?
我想的是,一個節(jié)點的鏈或多或少是一個鏈表,其中每個塊都有一個不可變的哈希。完成此操作后,您只需要一個安全的分布式數(shù)據(jù)庫即可存儲數(shù)據(jù)。什么是古老的分布式數(shù)據(jù)庫?好吧,每個人都有一個分布式數(shù)據(jù)庫,沒人知道!我說的是DNS。是的,它是分布式的,它存儲數(shù)據(jù)。每個人都有一個DNS服務。我意識到這不是預期的用途,但讓我們一起玩吧。
該協(xié)議的工作流程是受信任的機構將數(shù)據(jù)寫入DNS。每個記錄都有一個唯一的鍵,該鍵是內(nèi)容的哈希值。這意味著,通過更改數(shù)據(jù),您將更改ID,并且指向該ID的所有子代都將不一致。此外,DNS協(xié)議是分布式的,因此許多服務器之間共享數(shù)據(jù)的許多副本,這意味著您的一個DNS將脫機,而另一個將繼續(xù)為數(shù)據(jù)提供服務。還請考慮DNS被廣泛緩存,這使您的通信性能高(使用不可變數(shù)據(jù)緩存永遠不會成為問題)。
該系統(tǒng)使用所有公司都已經(jīng)擁有的DNS作為存儲,因此無需任何額外費用。DNS本身是一個分布式數(shù)據(jù)庫。
現(xiàn)在我們已經(jīng)定義了存儲數(shù)據(jù)的位置,我們只需要了解如何存儲數(shù)據(jù)即可。下一步是定義一個通信協(xié)議,使所有各方都可以扮演自己的角色。下圖顯示了流程。
DNS區(qū)塊鏈工作流程。
在上圖中,我們有:
- 在DNS上發(fā)布的推力實體。它是寫作的關鍵-其他人可以寫記錄,但是它們是無法理解的。
- 一個消費者,即推力生產(chǎn)者和讀取數(shù)據(jù)
- 數(shù)據(jù),其 可以是任何JSON數(shù)據(jù)。您可以選擇將其公開或不公開。
如何實施
現(xiàn)在我們知道該怎么做,并且已經(jīng)有了啟動該工具的工具,我們只需要使用源代碼即可。
為了使用DNS實現(xiàn)區(qū)塊鏈,我們必須面對一些重要問題:
- DNS限制-DNS并非旨在存儲大數(shù)據(jù)。我們想使用TXT記錄,但是它們只有254個字符。如果我們要存儲一個大的JSON對象,這是一個很大的限制。
- 安全性-即使我們想保持數(shù)據(jù)公開,DNS使用的UDP協(xié)議也存在問題。它沒有經(jīng)過加密,并且沒有像HTTPS協(xié)議中那樣可以推動授權的證書機制。
- 數(shù)據(jù)是按設計公開的—這可能是一個問題。
所有這些方面都有一個解決方案,并且您將看到,它很容易實現(xiàn)。實際上,通過使用加密技術和獨創(chuàng)性,我們將為上述所有問題找到一個明智的解決方案。
讓我們看看它是如何工作的。
創(chuàng)建沙盒環(huán)境
第一步是創(chuàng)建一個我們想玩的沙盒環(huán)境。我們需要啟動該工作的是帶有API系統(tǒng)的本地DNS服務器。我們通過創(chuàng)建一個托管該文件的docker-compose文件來實現(xiàn)這一目標。我使用了一個Visual Studio項目,在其中創(chuàng)建了一個我們將用于驗證數(shù)據(jù)的Web應用程序,一個將成為我們核心的庫以及一個測試項目。結果如下:
DNS區(qū)塊鏈項目
通過運行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
現(xiàn)在我們有了一個可以正常工作的本地DNS,我們可以創(chuàng)建一個可以通過API管理DNS記錄的客戶端。
DNS客戶端
第二步是包裝要在應用程序中使用的DNS客戶端功能。我想在這里做的是將來有能力更改DNS服務,因此我創(chuàng)建了一個接口和一個類實現(xiàn)。以下代碼片段顯示了該界面:
- public interface IDNSClient
- {
- Task<DNSEntry> GetRecord(string host, string zone);
- Task<bool> AddRecord(DNSEntry entry);
- Action Init { get; set; }
- }
如您所見,客戶端實現(xiàn)執(zhí)行HTTP調(diào)用來存儲記錄。您可以在本文末尾的GitHub項目中找到完整的類實現(xiàn)。我們僅為薩滿實施了本地提供商,但很容易對其進行擴展以支持大多數(shù)現(xiàn)代托管提供商上的任何商業(yè)DNS。
區(qū)塊鏈服務
現(xiàn)在我們已經(jīng)有了執(zhí)行部分,是時候?qū)崿F(xiàn)業(yè)務邏輯了。所有工作都在客戶端完成,客戶端將計算要存儲的數(shù)據(jù)并調(diào)用DNS客戶端方法以保留記錄。服務層由兩部分組成:
- BlockChainNode:節(jié)點的表示形式
- BlockChainService:實現(xiàn)邏輯的服務
讓我們詳細了解這些類如何工作。
區(qū)塊鏈節(jié)點
這是帶有JObject屬性的簡單類,用戶可以在其中存儲任何數(shù)據(jù)。它計算密鑰哈希數(shù)據(jù)。數(shù)據(jù)包含歷史記錄,該歷史記錄是到父項的鏈接。僅更改數(shù)據(jù)的字節(jié)將更改密鑰,這將導致以下節(jié)點不一致。以下代碼顯示了該類最重要的部分。
- 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();
- }
- }
- }
該代碼最相關的部分是:
- 數(shù)據(jù)對象:用戶可以在其中存儲數(shù)據(jù)的JSON對象
- 歷史記錄:與數(shù)據(jù)同步的可觀察列表(“歷史記錄”中的任何更改都會更改_history節(jié)點,反之亦然。)
- 哈希:根據(jù)數(shù)據(jù)的文本表示形式的MD5計算得出的哈希。結果以Base32算法編碼-類似于Base 64,但僅使用四個字節(jié)且僅包含小寫字符。這是因為DNS不區(qū)分大小寫,并且使用廣泛使用的Base64編碼產(chǎn)生了不一致的數(shù)據(jù)。
現(xiàn)在我們有了模型,我們必須繼續(xù)下一步:由服務實現(xiàn)的業(yè)務邏輯。
區(qū)塊鏈服務
區(qū)塊鏈服務實現(xiàn)用于保存,讀取和驗證記錄的方法。困難的部分是要解決DNS服務器記錄長度的255個字符的限制。解決方案是在Base64中對內(nèi)容進行編碼,然后使用命名約定將其拆分成塊保存在不同的記錄中。密鑰用作URL的一部分。因此,對于該項目mykey.domain.dom,我們將有0.mykey.domain.dom,1.mykey.domain.dom等下一段代碼顯示了節(jié)能方法。
- 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
- });
- }
從上一個調(diào)用的片段中可以看到WriteDNSFragmentedText,輸入文本被拆分,數(shù)據(jù)被保存在許多DNS條目中。
讀取數(shù)據(jù)是相反的。我嘗試獲取子記錄0,1,2,依此類推,直到有數(shù)據(jù)為止。一旦我收集了所有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;
- }
客戶端可以輕松地驗證所獲取的數(shù)據(jù)是否生成密鑰并且是否有效,因為客戶端可以獲取數(shù)據(jù),哈希并比較結果。此外,客戶端可以遞歸驗證以檢查所有父節(jié)點是否都是真實的。這就是驗證過程所要做的。它由下一部分代碼表示:
- 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();
- }
如您在上一個片段中看到的那樣,將驗證記錄,然后下載所有層次結構并檢查數(shù)據(jù)一致性。
現(xiàn)在我們了解了如何從DNS寫入和讀取數(shù)據(jù),下一步是如何確保它們的安全。
密碼學和密鑰
我們的系統(tǒng)可以向DNS讀取和寫入數(shù)據(jù),現(xiàn)在該注意安全了。我們假設寫給我們的DNS的人是受信任的,但是我們不能確保惡意的DNS服務器不會給我們偽造數(shù)據(jù)或有人不會讀取它(請記住,DNS數(shù)據(jù)是公共的)。
我在這里所做的就是對協(xié)議進行了以下改進:使用非對稱算法對存儲的數(shù)據(jù)進行加密存儲。這樣可以確保只有數(shù)據(jù)生產(chǎn)者才能生成消費者可以理解的數(shù)據(jù)。任何人都可以創(chuàng)建偽造的DNS服務器,但是他們將無法對待您偽造數(shù)據(jù)。而且,數(shù)據(jù)現(xiàn)在已加密,沒有人可以讀取。
非對稱算法是完美的,因為它只允許一定數(shù)量的讀者理解消息,但是只有消息源才能產(chǎn)生消息。為此,客戶端生成一對密鑰。公鑰用于加密數(shù)據(jù),因此生產(chǎn)者可以安全地保護它。與使用者共享用于解密的私鑰。可以手動共享它,例如通過電子郵件將其發(fā)送到加密的存檔中,或者發(fā)布在HTTPS網(wǎng)站上,證書可以在該網(wǎng)站上向用戶展示權限。
順便說一下,這個概念很簡單:現(xiàn)在數(shù)據(jù)已加密,沒有人可以代表我們寫入數(shù)據(jù)。但是還有另一個問題。對稱算法只處理少量數(shù)據(jù)(1024-4096字節(jié)),但我們必須處理巨大的JSON有效負載。我們有兩種方法:
- 將完整的消息分成小字節(jié)塊,并一一加密/解密它們。
- 創(chuàng)建一個對稱密鑰,使用生成的密鑰對數(shù)據(jù)加密,然后使用非對稱對對生成的密鑰進行加密。這樣,每個記錄都具有用于加密數(shù)據(jù)的不同對稱密鑰。該密鑰是公開共享的,但只有擁有私有密鑰的人才能使用。
考慮到對所有字節(jié)塊進行編碼的計算量,我使用了第二種解決方案。這將我們帶到下一個有效負載:
- {
- "data":"json object encrypted with the symm key",
- "key":"symm key encripted with the aymm alghorithm"
- }
在上面的代碼段中,我們可以看到存儲在JSON有效負載中的加密數(shù)據(jù)和解密密鑰。讀取器將使用私鑰解密對稱密鑰,然后將其用于解密數(shù)據(jù)。
代碼中的更改是最小的:所需的只是包裝\展開數(shù)據(jù)的附加步驟。
在下一個代碼段中,我顯示了完成數(shù)據(jù)生成的步驟:
- #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.
既然我們已經(jīng)完成了有關區(qū)塊鏈實現(xiàn)的說明,那么我們就擁有了將數(shù)據(jù)存儲在DNS區(qū)塊鏈中的所有詳細信息。
結論
即使將DNS服務器用作數(shù)據(jù)庫看起來似乎很聰明,但事實并非如此。DNS并非旨在以這種方式存儲數(shù)據(jù)。
如果我們必須處理安全的不可變數(shù)據(jù),則解決方案是使用標準的區(qū)塊鏈平臺,我的意思是需要使用一個真正的區(qū)塊鏈系統(tǒng)。
無論如何,嘗試實現(xiàn)無服務器的區(qū)塊鏈非常有趣,我希望它教會了我們區(qū)塊鏈平臺背后的原理。