區塊鏈不僅是一個流行詞。它也不限於加密貨幣和比特幣。憑藉其創造透明度和公平性的能力,這項技術正在革新各個領域。應用範圍從跟蹤系統到保護資料,再到執行線上投票系統。它可以幫助實施反洗錢跟蹤系統,或者簡單地跟蹤您在商店購買的產品的來源。
就像資訊科學中經常發生的那樣,許多區塊鏈平臺管理著所有的複雜性,使我們可以像儲存一個簡單的資料庫一樣簡單地儲存資料。
在本文中,我想實現一個區塊鏈資料庫,以瞭解此類解決方案的關鍵要素。而且,為了使其更具挑戰性,我將在不使用任何資料庫或伺服器的情況下做到這一點。
該解決方案可以輕鬆地使您擁有可以驗證並安全儲存的不可變資料。
這篇文章的結構如下:
什麼是區塊鏈資料庫及其使用方式
如何僅使用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) returnnew 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並非旨在以這種方式儲存資料。
如果我們必須處理安全的不可變資料,則解決方案是使用標準的區塊鏈平臺,我的意思是需要使用一個真正的區塊鏈系統。
無論如何,嘗試實現無伺服器的區塊鏈非常有趣,我希望它教會了我們區塊鏈平臺背後的原理。
作者:鏈三豐,來源:區塊鏈研究實驗室