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