Kendi Blockchain Sistemini Tasarlamak — 1: Merkeziyetsiz Ağ Tasarımı
Merhabalar,
Bu yazımda Decentralized Network (Merkeziyetsiz Ağ) kavramını detaylıca ele alacağız ve kendi merkeziyetsiz ağımızın temellerini kodlayarak oluşturacağız.
Merkeziyetsiz Ağ denilince insanın aklında sunucuların olmadığı ve kapalı ağda bulunan tüm kullanıcıların (Client) peer to peer biçimde (Arada hiçbir sunucu olmaksızın) birbirleriyle doğrudan haberleştiği gibi absürt bir konsept geliyor.
Torrent, tüm clientların doğrudan iletişime geçtiği dağıtık (P2P) konseptinin bir benzerini uygulasa da (Bkz: Seed Mechansim) burada bahsettiğimiz “Merkeziyetsiz Ağ” tanımı aslında bir isim oyunu. Merkeziyetsiz ağ terimini “Çok merkezli ve herkesin kendi merkezini kurup dahil olabileceği ağ” olarak tercüme etmek daha doğru olacaktır.
1. Tek/Çok merkezli olmak
Bir banka müşterisi olduğunuzu düşünün. Bildiğiniz üzere bir banka, kendi veri merkezine sahiptir ve verileri üzerindeki tek otorite de kendisidir. Kayıt defterini, veri tabanını kendisi tutar.
Daha basit anlaşılması adına, Görsel 1'de en soldaki şema, tek merkezli bir ağı ifade etmektedir. Siz bankanızın mobil uygulamasını açtığınızda, telefonunuzdaki uygulama banka sunucusuna sizin kimlik bilgilerinizle bir istek gönderir ve sizin bakiye, kişisel bilgileriniz vs. sunucudan telefonunuza gönderilir.
Merkezdeki nokta, bankanın veri merkezi (Sunucusu) iken telefonlar, bilgisayarlar veya ATM’ler ise ona doğrudan bağlanan çevresindeki noktalar olarak ifade edilir.
Esasen bu yaklaşım, günümüzde en yaygın kullanılan ağ programlama tekniğidir. Bu kapsamda sizin hesabınızdaki bakiye, bankanın dinazor stack olarak kullandığı x bir ilişkisel veri tabanında, sizi ifade eden satırda bulunan bir field’dan başka bir şey değildir.
Bu, sıradan bir client-server konseptidir ve genellikle yazılım camiasının %99'u da bu yaklaşım üzerine iş/proje geliştirir. Dolayısı ile Bitcoin, kripto paralar ve NFT kavramı ile hayatımıza giren ve günümüzde sosyal medyadan tutun veri depolamaya kadar geniş bir spektrumda kendine kullanım sahası yaratan bu merkeziyetsiz ağ konsepti neden gerekli olsun ki ?
2. Neden Merkeziyetsiz Ağlara İhtiyaç Duyarız ?
Yukarıda tek merkezli ağ konseptini konuşurken neden merkeziyetsiz ağlara ihtiyaç duyduğumuzu da aslında anlayabiliyoruz.
Geçmişte birçok defa devletler, bankalar ve güvenilir kuruluşlar insanları mağdur etti.
Örneğin bir devlet, kendi işine gelecek şekilde kontrolsüzce para basabilir. Faiz oranlarıyla oynayabilir. Bu para birimini kullanan vatandaşlar ise korkunç bir değer kaybı ile yüzleşmek zorunda kalırlar. Günün sonunda her şey merkezi otoriterin işine geldiği gibi olur ve insanlar çoğunluğu oluştursa bile güçlü azınlığa karşı koyamazlar.
Aynı şekilde, geçmişte birçok bankanın iflas etmişliği, insanların birikimlerinin ziyan olduğu görülmüştür.Bazen ise teknik olumsuzluklar sebebiyle, kendi iradeleri dışında kullanıcıları mağdur etmişlerdir.
Örnek vermek gerekirse birkaç ay önce yerli bir borsa yatırım uygulaması, yaklaşık 24 saat kapalı kalarak kullanıcılarının telafisiz zararlara uğramasına sebep olmuştu.
Özetle birçok insan devletlere, şirketlere, yani otoritelere güvenmiyor. Ve merkeziyetsiz ağlar, yazılımsal ihtiyaçtan ziyade merkezi otoriteye duyulan bu güvensizliğin bir sonucu olarak geliştirilmiştir.
Yakın geleceğe kadar dünyadaki finans sektörünün tamamı merkeziyetli ağlar ile çalışmaktaydı. Şimdi ise insanların önemli bir bölümü varlıklarını korumak için değeri yalnızca talep sayesinde oluşan ve garantörlüğünü kimliği belirsiz binlerce gönüllünün oluşturduğu sanal bilgisayar ağlarını tercih ediyorlar.
3. Ağ dediğimiz nedir ?
Ağ dediğimiz kavram, birbiriyle iletişime geçen makinelerdir. Bir ağ oluşturmak için makinelerin birbirine doğrudan donanımsal olarak bağlantılı olması veya dış dünyadan yalıtılmış bir internet ağında bulunması gerekmez. Makinelerin birbirleri ile yetkilendirilmiş bir iletişim içerisinde olması yeterlidir.
Genel internet (WAN) üzerinde “makinelerin haberleşebildiği” bir ağ denildiğinde akla genelde ilk VPN (Virtual Private Network) gelir. Ancak bunlar esasen VPN’ler de bahsettiğimiz prensiple çalışan ve merkeziyetli (Sahipli) ağlardır.
VPN dediğimiz sistem ise sizin paketlerinizi, kendisine bağlı diğer bilgisayarlara veya onlar üzerinden dış dünyaya yönlendiren ve bu iletişimi çift yönlü hale getiren sunucular (Ve onların yazılımları)dır.
Dışarıdan bakarsak, LAN (Locale Area Network) de bir merkeziyetsiz ağ değildir. Çünkü günün sonunda paketlerinizin ağ içerisinde yönlendirilmesinden sorumlu ve sizi kimliklendiren/yetkilendiren router’lar vardır. Donanımsal haberleşmenin olduğu bir ortamda merkeziyetsiz bir yapının olması pratikte mümkün değildir.
Bu nedenle “Merkeziyetsiz Ağ” kavramı, network layer’da değil, application layer’da olabilir. Yani ağ, makinelerin birbiri donanımsal iletişiminden ziyade soyut bir işbirliği içinde olmasıdır.
Yani siz, VPN’i özgür bir ağ kabul ederseniz ,VPN ağınızda blockchain ağı oluşturabilirsiniz. Aynı şekilde, LAN üzerinde çalışan bir Blockchain ağı da oluşturabilirsiniz.
Burası biraz kafa karıştırıcı olabilir ancak basitçe açıklamam gerekirse; arkadaşlarınızla internet kafede Counter Strike odası kurup oynadığınızı hayal edin. Oyundaki odanın yöneticisi, internet kafenin modemi veya internet kafe sahibi olmaz. Oyunu kuran kişi olur.
Bir server-client iş mantığı çerçevesinde diğer oyuncuların, oyun başlatan sunucunun bilgisayara server (Host) olarak bağlanması ve bu sayede oyunun birlikte oynanması sağlanır. Tüm diğer client oyuncular, server (Host) olan oyuncunun bilgisayarına komutlarını gönderir, bu komutlar Host bilgisayarda işlenir ve odadaki herkese sonuçları gönderilir.
Oyun bittiğinde, bir sonraki oyunda ise bir başka kişi oda kurup Host olabilir. Bu örnek, merkezin (Host/Sunucu) el değiştirebildiğinin anlaşılması açısından oldukça önemli. Ağ, internet kafenin yerel ağı ancak onun içerisinde biz kendi odamızı (Counter Strike ağımızı) kurmuş olduk.
4. Kendi Ağımızı Nasıl Oluşturabiliriz ?
Ağ, temel olarak birbiriyle iletişim kuran bu makinelerin bir araya gelmesiyle oluşur. Bir ağ oluşturmak için, makinelerin doğrudan donanımsal olarak bağlantılı olması veya dış dünyadan yalıtılmış olması gerekmez. Aksine, VPN ağları, makinalar arasında bir sunucu aracılığıyla köprü kurduğunu varsayarsak, internet (WAN) gibi bilinen bir ağın ötesinde sahipli bir ağ konsepti sunabilir.
Ağ dediğimiz kavram, neticede birbiriyle iletişime geçen makinelerdir. Bir ağ oluşturmak için makinelerin birbirine doğrudan donanımsal olarak bağlantılı olması veya dış dünyadan yalıtılmış bir internet ağında bulunması gerekmez. Makinelerin birbirleri ile yetkilendirilmiş bir iletişim içerisinde olması yeterlidir.
İleride detaylıca değineceğiz ancak Blockchain ekosisteminde biz verileri işleyen, birbirlerine ileten ve daima haberleşen bu sunuculara Node diyoruz.
Kendi Node’unuzla daima etkileşimde bulunan Node’lar, WAN üzerinde bir ağ oluşturmanızı sağlar.
5. Ağdaki Node’lar Birbirlerini Nasıl Bulabilirler ?
Blockchain ekosisteminde ağ, bir Node kümesidir ancak bir boş küme değildir. Dolayısıyla ağdaki ilk elemanın, ağı yaratan kişinin ayağa kaldıracağı Node olması gerekir.
Bir Node yazılımı geliştirdiğimizi varsayalım. Üç Node daha katılırsa, dört Node’dan oluşan bir merkeziyetsiz ağımız olmuş olur. Peki ağa katılma dediğimiz işlem nedir ve nasıl gerçekleşir ?
Bunun için birkaç farklı yaklaşım mevcuttur. Bunların bazısı, başlangıçta ağın sahipli (Merkeziyetli) şekilde başlamasını ancak zamanla merkeziyetsizleşmesini sağlarken, bazıları ise doğrudan merkeziyetsiz bir ağ başlangıcı imkanı sunar.
Bu yazı kapsamında en sık kullanılan;
- Bootstrapping Nodes (Well-Known Endpoints)
- Network Traversal (Bruteforce)
- Announcement Broadcast (Peer to Peer Shared Memory)
- Distributed Hash Tables
Node bulma tekniklerini inceleyeceğiz.
6.1. Bootstrapping Nodes (Well-Known Endpoints)
Bootstrapping nodes (Başlangıç düğümleri), özellikle dağıtık ağlar ve merkeziyetsiz sistemlerle ilgili en çok kullanılan metodlardan biridir. Bootstrapping düğümleri, bir merkeziyetsiz ağın veya blockchain ağının başlangıç aşamasında, ağa katılmak isteyen yeni düğümlerin ağa bağlanmasına yardımcı olan özel düğümleri veya sunucuları ifade eder.
Bootstrapping Nodeların görevi, ağdaki diğer Node’ların endpointlerini istiflemek ve yönetmektir. Ağa yeni bir Node katıldığında kendisini Bootstrapping Node’a tanıtır. Bootstrapping Node’da yeni katılan Node’u diğer Node’lara duyurur ve aynı şekilde, diğer Node’ların endpointlerini de yeni Node’a iletir.
Bootstrapping Node’ları bu iş için görev alan özel Node’lardır. Bir ağı gerçekten de “boot” ederler. Zira onlar olmadan ağa yeni bir node katılamaz çünkü hangi endpointlerle alışveriş yapacaklarını bilmeleri mümkün değildir.
6.2. Network Traversal (Bruteforce)
VPN, LAN gibi kapalı ağlarda IP aralığı belirli olduğu için Network Traversal algoritması kullanılabilir. Network Traversal mekanizması, Bootstrapping Nodes gibi daha merkezci yaklaşımları ekarte etmek için geliştirilmiştir ve ağ topolojisinin sınırlı olması sayesinde ölçeklenmesi mümkün olan bir modeldir.
Dolayısı ile Network Traversal, bir çeşit bruteforce (Deneme yanılma) mekanizmasıdır. Ağın sınırlı olduğu durumlarda kullanılabilir. Ağdaki tüm IP adreslerine paket gönderir ve beklenen bir cevap alırsa, ilgili ip adresini bir düğüm endpoint’i olarak hafızasına kaydeder. Bunu belirli aralıklarla yaparak yeni eklenen düğümlerin de farkedilmesi sağlanır.
Örnek vermek gerekirse, kapalı ağlarda hatırlayacağınız gibi Router, herkese yerel ağda paket alabilmesi için yerel bir IP tahsis eder. Bunu aynı binada bulunan posta kutusu numaralarına benzetebiliriz.
Peki, kendi yerel ağınızda sizin gibi bir Node yazılımı çalıştıran diğer makineleri nasıl bulabiliriz ?
Elbette tüm posta kutularına mektup bırakarak !
Hangisi mektubumuzu açıp okur ise, amacımıza ulaşmış oluruz. Okunmayan ve bırakıldığı yerde kalan mektupların ise bize bir zararı olmayacaktır. Elbette posta kutusu uçsuz bucaksız bir sitede (yani WAN’da) değilseniz.
Bunu çok büyük veya limitsiz ağlarda denemek mantıksızdır. Çünkü denenebilecek IP adresleri için devasa bir olasılık havuzu vardır ve hepsine paket gönderip dönüt almak, pratik olarak imkansızdır.
Bu nedenle Network Traversal; VPN, LAN gibi kapalı ağlarda, IP aralığı belirli olduğu için kullanılabilir.
6.3. Announcement Broadcast (Peer to Peer Shared Memory)
P2P ağlarda, ağa dahil olmak için ağdaki en az bir düğümün endpoint (IP/PORT) bilgisine sahip olmak gerekmektedir. Ağa katılmak için, ağda bulunan düğüme katılma başvusu yapılır. Başvurudan kasıt, ağa katılmak için gerekli RPC (Remote Procedure Call) fonksiyonunun çağırılması veya sıradan bir http paketi gönderilmesi dahi olabilir. Bunun dışında, doğrudan UDP veya TCP protokolleri üzerinden çalışan veya bunları kullanarak kendi protokollerini geliştiren düğüm yazılımları da mevcuttur.
Ağdaki düğümler, ağdaki diğer tüm düğüm sunucuların endpointlerini zaten tutmaktadırlar. Dolayısıyla başvuru yapılan düğüm, elinde bulunan ve ağdaki diğer düğümlerin endpointlerini içeren listeyi başvuru yapan düğüm ile paylaşır.
Buna ek olarak yeni düğümün başvurduğu düğüm; ağdaki diğer düğümlerin tamamına yeni katılan düğümün endpoint bilgisini de iletir. Sonuç olarak ağdaki tüm diğer düğümlerin endpoint’i yeni düğümde bulunurken ağdaki tüm diğer düğümlerde de yeni düğümün endpoint’i bulunur.
Burada bir döngü oluşmaması için çeşitli broadcast algoritmaları mevcuttur. Böylece yeni düğümün katılma sinyalini alan düğümler, sinyali kendisine gönderen düğüme tekrar sinyalin gitmesini sağlayacak bir kısır döngü oluşmasının önüne geçerler.
6.4. Distributed Hash Tables
Hash tabloları, verileri key-value çiftleri şeklinde depolayan bir veri yapısıdır. Verileri istiflemek, diske yazmak, okumak veya verilere erişmek birçok programlama dilinde dahili arayüzler vasıtası ile otomatik olarak gerçekleştirilir.
Örneğin javascript ile bir hash table oluşturmak için boş bir obje tanımlamak yeterlidir.
let hashTable = {};
hashTable["key_1"] = "value 1";
hashTable["key_2"] = "value 2";
Yüksek seviyeli programlama dillerinin çoğu, oluşturduğunuz key-value objeleri arkaplanda hash table olarak istiflemekte ve işletmektedir. Hash tabloları, O(1) zaman karmaşıklığına sahip oldukları için performans açısından da oldukça verimlidir.
Yukarıda, görsel 6.4.1'de bir hash tablosunun indexlenmesi ve key-value pairs olarak tutulması gösterilmektedir.
Sonuç olarak Hash tabloları, oluşturulan düzensiz objeleri istiflemek ve kullanmak için mükemmel bir seçenektir.
- Anahtar-Değer İlişkisi: Her öğe (veri) bir anahtar (key) ile ilişkilendirilir. Anahtar, öğeyi tanımlayan benzersiz bir değerdir.
- Hash Fonksiyonu: Anahtarlar, bir hash fonksiyonundan geçirilir. Hash fonksiyonu, anahtarın bir tam sayıya dönüştürülmesini sağlar. Bu dönüştürülmüş tam sayı, veriyi depolamak veya aramak için kullanılan indeks olarak kullanılır.
- Hızlı Erişim: Hash tablosu, veriyi hızlı bir şekilde erişmenizi sağlar. Anahtara karşılık gelen hash değeri hesaplandığında, bu hash değeri tablonun indeksi olarak kullanılır ve ilgili öğeye hızlı bir şekilde erişilir.
Aynı biçimde, Distributed Hash Tables (DHT) metodunu kullanan bir ağa katıldığınızda, otomatik dönüt olarak bir hash table alırsınız. Bunu genellikle dağıtık ağ depolama dizini veya ağda görev yapan reverse proxy’ler vasıtası ile Node’unuza iletilir.
Burada vurguyu barındıran Distributed kavramı ise her key’e ait value sorumluluğunun ağdaki node’lar arasında dağıtılmış olmasıdır. Dolayısı ile tablonun farklı bölümleri, farklı Node’lardan alınıp birleştirilir. Bu işlem network lookup olarak isimlendirilir.
Bunun için genellikle Chord, Kademlia veya CAN gibi yaygın DHT protokolleri kullanılabilir.
7. Kendi Node Yazılımımızı Oluşturalım
Farklı blockchain ağları kendi özelleşmişNode yazılımlarına sahiptir.
Örneğin GetH, Parity veya Nethermind; Go’da yazılmış Ethereum sunucu yazılımlarıdır. Bilgisayarınızda veya uzak sunucunuzda bunlardan birini edip çalıştırırsanız, bilgisayarınız Ethereum ağında bir Node’a dönüşür.
Aynı şekilde, kendi ağımızı ve blockchain sistemimizi tasarlayacaksak bizim de kendi Node sunucumuza ihtiyacımız var. Normalde kurumsal yapılar, katıldıkları ağların Node yazılımını kullanırlar. Ve bu Node yazılımları üzerinde kendi ihtiyaçlarına karşılık verecek şekilde bir takım düzenlemeler yapabilirler.
Biz, tamamen sıfırdan bir blockchain sistemi ve sıfırdan bir ağ geliştirmek istediğimiz için kendi Node yazılımımızı da sıfırdan geliştireceğiz. Elbette GetH client kadar kapsamlı bir şey olmayacak lakin son derece basit ve yüzeysel bir sunucu yazılımı tasarlayıp, blockchain ve merkeziyetsiz ağ konseptlerini uygulayacağız.
Öncelikle her Node’un bir sunucu olduğunu unutmayın. Bu yazıda merkeziyetsiz ağ oluşturmak için 6.3'cü maddede bahsettiğimiz Announcement Broadcast mekanizmasını kullanacağız.
Yani ağdaki tüm Node’lar aynı zamanda bir bootstrapping node vazifesi görecektir.
Uygulamamızı C# kullanarak geliştireceğiz. Dotnet CLI kullanarak projemizi oluşturalım;
dotnet new console
Blockchain Node’larında haberleşme için yaygın olarak kullanılan protokol, JSON-RPC’dir. Standart HTTP protokolüne göre daha hafif olması, json vasıtası ile veri iletişimisağlaması ve performansı yüksek bir protokol olması sebebiyle Nodelar arası iletişim için biz de JSON-RPC tercih edeceğiz.
Veri istiflemesi için json ve yaml kütüphanelerine ihtiyaç duyacağız. DotNet CLI ile gerekli NuGet paketini yükleyelim.
dotnet add package Newtonsoft.Json --version 13.0.3
dotnet add package YamlDotNet --version 13.4.0
İlk olarak geliştirdiğimiz Node (Sunucu) yazılımı için bir model tanımlayalım. Bu modeli uygulamamız başladığında, yaml parser kullanarak dolduracağız. Modelimizde port numarası (Integer), yazılım versiyonumuz (Double) ve bağlanacağımız peerlerin endpointleri (Array) yer alacak. Bu modeli, uygulamamızın RPC sunucusu oluşturmak ve açılışta peerlarla iletişime geçmek için kullanacağız.
— — — — —
models/RuntimeConfig.cs
using YamlDotNet.Serialization;
namespace BlockchainNetwork;
class RuntimeConfig
{
[YamlMember(Alias = "server_ip")]
public required string ServerIp { get; set; }
[YamlMember(Alias = "port")]
public required int Port { get; set; }
[YamlMember(Alias = "node_version")]
public required double NodeVersion { get; set; }
[YamlMember(Alias = "node_api_path")]
public required string NodeApiPath { get; set; }
[YamlMember(Alias = "metadata_path")]
public required string MetaDataPath { get; set; }
[YamlMember(Alias = "peer_endpoints")]
public required List<string> PeerEndpoints { get; set; }
public bool IsGenesisNode {get; set;}
}
Ardından bu modeli yaml dosyasından dolduran bir Utility sınıfı yazalım.
— — — — —
utility/config.cs
using System.Data;
using YamlDotNet.Serialization;
namespace BlockchainNetwork;
class Config
{
public static RuntimeConfig RuntimeConfig {get; private set;}
public static void Load()
{
string yamlFilePath = "../config.yaml"; // Replace with the path to your YAML file
try
{
// Create a deserializer
var deserializer = new DeserializerBuilder().Build();
// Read the YAML file content
string yamlContent = File.ReadAllText(yamlFilePath);
// Deserialize the YAML into your C# class
RuntimeConfig config = deserializer.Deserialize<RuntimeConfig>(yamlContent);
// Decide Node Is Genesis
config.IsGenesisNode = IsGenesisNode(config);
Config.RuntimeConfig = config;
}
catch (Exception e)
{
throw new DataException($"Error: Runtime Config Cannot Loaded {e.ToString()}");
}
}
private static bool IsGenesisNode(RuntimeConfig config)
{
return (config.PeerEndpoints.Count == 1 && config.PeerEndpoints[0] == "GENESIS");
}
}
Modülümüzü uygulama açılırken çalıştırmak istiyoruz. Bunun için program.cs içerisindeki main fonksiyonunun içine
Config.Load();
satırını ekleyelim.
Eğer ağın ilk Node’unu ayağa kaldırıyorsak (Genesis Node) elbette verebileceğimiz başka bir node endpoint’i yok. Bu kondisyonu da burada işliyoruz:
private static bool IsGenesisNode(RuntimeConfig config)
{
return (config.PeerEndpoints.Count == 1 && config.PeerEndpoints[0] == "GENESIS");
}
Böylelikle Config.yaml’da peer_endpoints field’ının değerinin “GENESIS” olması durumunda projemiz ilk peer olarak ayağa kalkacaktır.
Ayrıca unutmamamız gereken bir detay var. Uygulamamız; peer endpointlerini elde ettikçe yerel depolamaya yazmalı. Uygulama açılışında eğer yerel depolamada peer endpointleri yoksa, doğrudan configde yer alan endpointler kullanılmalı. Eğer peer endpointleri varsa, yerel depolamada yer olanlar kullanılmalı.
İlk olarak peer endpointleri için bir model tanımlayacağız.
— — — — —
models/PeerModel.cs
using System.Data;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace BlockchainNetwork;
class PeerModel
{
public string ipAddress {get; private set;}
public int port {get; private set;}
public double version {get; private set;}
public string apiPath {get; private set;}
public string serverHash {get; private set;}
public bool trust {get; private set;}
public string GetEndpoint()
{
return $"{ipAddress}:{port}/{apiPath}";
}
private string endpointParserPattern = @"^(.*?):(\d+)/(.*)$";
[JsonConstructor]
public PeerModel(string ipAddress, int port, double version, string apiPath)
{
this.ipAddress = ipAddress;
this.port = port;
this.version = version;
this.apiPath = apiPath;
this.serverHash = Crypt.HashSha256(this.ipAddress);
this.trust = true;
}
}
IpAddress, port ve apiPath zaten peer’a Json-Rpc isteği atabilmemiz için gerekli.
Bazı blockchain node yazılımlarında, yalnızca aynı versiyondaki node’lar birbirleriyle iletişim kurmaktadır. Bu durumda bir ağ içinde yalnızca kendi arasında iletişim kuran kümeleşmeler (Alt ağlar) oluşabilir. Kendi yazılımımızda bunu opsiyonel olarak bırakacağız ancak yine de version bilgisini peer modelimize ekleyelim.
Trust, bir node’un; node’umuza gönderdiği verilerin işlem göreceğini veya gözardı edileceğini belirliyor. Güvensiz işlem yaptığı tespit edilen peerların ip sini untrusted olarak işaretleyebiliriz. Böylelikle Ip Block gibi çalışan bir mekanizma ile güvensiz bir Node’u dışarlayabiliriz.
Bizim gibi, diğer Node’lar da bu dışarlamayı yaparsa günün sonunda Node aslında bir nevi ağdan çıkarılmış olacaktır.
ServerHash ise node’un ip adresini kullanarak oluşturduğumuz bir hash sadece. Sunucuyu benzersiz olarak yerelde kimliklendirmemizi ve ip yerine bir adres ile ifade etmemizi sağlıyor.
Oluşturduğumuz peer’a kolayca istek atabilmek için getEndpoint adında bir fonksiyon implemente edelim:
public string GetEndpoint()
{
return $"{ipAddress}:{port}/{apiPath}";
}
Bunlara ek olarak; hatırlayacak olursanız config.yaml’da doğrudan endpoint okuyorduk. Bu parametreler yerine direkt string url’den bunları parse eden bir constructor daha yazmalıyız:
public PeerModel(string peerUrl)
{
Match match = Regex.Match(peerUrl, endpointParserPattern);
if (!match.Success)
{
throw new DataException("Peer url is invalid");
}
//peer1.example.com:8080/jsonrpc
this.ipAddress = match.Groups[1].Value;
this.port = Convert.ToInt32(match.Groups[2].Value);
this.version = Config.RuntimeConfig.NodeVersion;
this.apiPath = match.Groups[3].Value;
this.serverHash = Crypt.HashSha256(this.ipAddress);
this.trust = true;
}
Bu arada yukarıda serverHash için mockup olarak kullandığım ama içersinden HashSHA256 fonksiyonunu kullandığımız Crypt utility sınıfını da oluşturalım.
— — — — —
utility/crypt.cs
using System;
using System.Text;
using System.Security.Cryptography;
namespace BlockchainNetwork;
public class Crypt
{
public static String HashSha256(String input)
{
using (SHA256 sha256 = SHA256.Create())
{
// Convert the input string to bytes
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
// Compute the hash
byte[] hashBytes = sha256.ComputeHash(inputBytes);
// Convert the hash bytes to a hexadecimal string
string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
return hashString;
}
}
}
Şimdilik sadece verilen metini sha256 algoritması ile hashleyip çıktısını yine bir string olarak döndüren basit bir fonksiyon implemente ettik.
Blockchain konusuna daha derin girdiğimiz ileriki dönemlerde, burada yeni fonksiyonlar ve işlevler ekleyeceğiz.
Tüm bunlara ek olarak, bu peer model’leri bir liste olarak memory’de tutacağız. Bunları diskte tutmak ve okumak istiyoruz. Öyle ise modelimiz json olarak çıktı vermeli ve verilen json’u constructor’undan alıp, kendi içeriğini doldurabilmelidir.
Bunun için json dosyası okuyan ve yazan bir Utility sınıfı oluşturalım.
— — — — —
utility/storage.cs
using System;
using System.IO;
using System.Text.Json;
namespace BlockchainNetwork;
class Storage
{
// Method to write JSON data to a file
public static void WriteToJsonFile(string filePath, object data)
{
// Serialize the data to JSON format
string jsonData = JsonSerializer.Serialize(data, new JsonSerializerOptions
{
WriteIndented = true // To format the JSON data with indentation
});
// Write the JSON data to the file
File.WriteAllText(filePath, jsonData);
}
// Method to load JSON data from a file
public static T? LoadFromJsonFile<T>(string filePath) where T : class
{
if (File.Exists(filePath))
{
// Read the JSON data from the file
string jsonData = File.ReadAllText(filePath);
// Serialize and return
T? data = JsonSerializer.Deserialize<T>(jsonData.ToString());
return data;
}
return default;
}
}
Bu, aslında projemiz boyunca birçok noktada ihtiyaç duyacağımız bir utility sınıfı. Her model, kendi serialize fonksiyonunu içerecek olsa da json olarak yazan ve okuyan bir storage sınıfına ihtiyacımız var.
Bir template ve veri sağlandığı taktirde Storage sınıfını kullanarak herhangi bir model veya repository’yi diske yazabilir veya diskten yüklenmesini sağlayabiliriz.
— — — — —
utility/rpcRequest.cs
Son olarak ileride ihtiyaç duyacağımız rpcRequest utility sınıfını implemente edelim.
using System.Net;
using Newtonsoft.Json.Linq;
class RpcRequest
{
// Send a json rpc request to single peer
public static async void Send(string endpoint, string method, JToken payload)
{
HttpClient client = new HttpClient();
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, endpoint);
JObject responseData = new JObject
{
["jsonrpc"] = "2.0",
["method"] = method,
["params"] = payload,
["id"] = 1
};
request.Content = new StringContent(responseData.ToString(), null, "application/json");
HttpResponseMessage response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
// Sends same content to all peers async
public static async void SendBroadcast(List<string> endpointList, string method, JToken payload)
{
try
{
String eachResponseData = new JObject
{
["jsonrpc"] = "2.0",
["method"] = method,
["params"] = payload,
["id"] = 1
}.ToString();
HttpClient client = new HttpClient();
List<HttpRequestMessage> requestList = new List<HttpRequestMessage>();
List<Task<HttpResponseMessage>> responseList = new List<Task<HttpResponseMessage>>();
endpointList.ForEach(eachEndpoint => {
HttpRequestMessage eachRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{eachEndpoint}")
{
Content = new StringContent(eachResponseData, null, "application/json")
};
responseList.Add(client.SendAsync(eachRequest));
});
IEnumerable<HttpResponseMessage> sendedResponseList = await Task.WhenAll(responseList);
foreach(HttpResponseMessage eachSendedResponseMessage in sendedResponseList)
{
eachSendedResponseMessage.EnsureSuccessStatusCode();
}
}
catch(Exception e)
{
Console.WriteLine($"Broadcast error: {e.ToString()}");
}
}
}
Bu sınıfın işlevi oldukça basit. Anlaşılabileceği gibi Send fonksiyonu tek bir peer’a bir jsonRpc isteği gönderirken, SendBroadcast fonksiyonu birden çok peer’a aynı anda jsonRpc isteği gönderebilir.
SendBroadcasst fonksiyonu ise parametre olarak tek bir endpoint değil, bir endpoint listesi alır. Herbirine göndereceği istekleri async olarak toplar ve topluca, asenkron olarak bu ağ işlemlerini işler. Yani burada bir nonblocking (Birbirini beklemeden çalışma) durumu sağlanarak performanstan kazanç sağlanır.
Burada sendBroadcast fonksiyonunu Announcement Broadcast tekniğini uygulayabilmek için geliştirdik. Hatırlayacağınız üzere Announcement Broadcast, peer to peer bir teknik. Peerlar arasında yayılım vasıtası ile çalışıyor ve böylelikle tüm ağa yayılmayı amaçlıyor.
Dolayısıyla bir Node’un ağımıza katılması için ağda aktif olarak çalışan en az bir Node’un endpoint’ini bilmesi gereklidir.
Bütün bağımlılıkları yazdık. Henüz bir JsonRpc server yapısı implemente etmedik ancak iş mantığını geliştirmek için peerService’i geliştirebiliriz.
— — — — —
services/peerService.cs
class PeerService
{
public List<PeerModel> peerRepository = new List<PeerModel>();
string peerPath;
İlk olarak, aktif olarak bellekte tuttuğumuz peer modellerini tutan bir repository yani bir liste tanımlayacağız.
Ardından ise uygulamamız çalıştığında peer bilgilerinin belleğe yükleneceği json dosyasının konumunu belirten bir değişken tanımlayacağız.
Constructor ile devam edelim;
public PeerService()
{
if(!Config.RuntimeConfig.IsGenesisNode && Config.RuntimeConfig.PeerEndpoints.Count != 0)
{
try
{
peerRepository = loadPeerRepositoryFromConfig(Config.RuntimeConfig.PeerEndpoints);
}
catch(Exception e)
{
throw new DataException($"Peers in config.yaml cannot be readed. Please check. {e.ToString()}");
}
}
this.peerPath = $"{Config.RuntimeConfig.MetaDataPath}/peers.json";
peerRepository = peerRepository.Concat(loadPeerRepository()).ToList();
}
Eğer Node’umuz config’te bir Node’a bağımlı olarak ayağa kalkmıyorsa ve genesis olarak tanımlanmışsa, GENESIS Node olarak ayağa kalkmalı.
Bunun dışında, config’te belirtilen yoldaki peers.json dosyasını model olarak parse edip repository’e ekleyecektir.
Json objesi olarak alıp peerService fonksiyonlarının rpc sunucusu modülümüzden çağırılabilmesini sağlayan bir fonksiyon implemente edeceğiz:
public JToken executePeerService(string methodName, JToken parameters)
{
MethodInfo? methodInfo = typeof(PeerService).GetMethod(methodName);
if(methodInfo == null)
{
throw new RuntimeBinderException($"method {methodName} not found");
}
Object? executionResult = methodInfo.Invoke(this, new object[] {parameters});
if (executionResult is JToken jTokenResult)
{
return jTokenResult;
}
throw new InvalidOperationException("Method error occured");
}
Özetlemek gerekirse, executePeerService fonksiyonu bir fonksiyon adı ve jsonRpc ile gönderilen parametreleri içeren bir json objesi olmak üzere iki adet parametre alıyor.
Burada jsonRpc ile invoke edilen method isminin, peerService.cs’deki ile aynı olduğu durumda ilgili fonksiyon, parametreler gönderilerek çalıştırılıyor ve sonucu döndürülüyor.
Bunu kendi geliştirdiğimiz bir controller mapping’e benzetebiliriz. Normalde MVC kullanırken endpoint’imize nasıl bir path assign ediyorsak, aynı şekilde; bu defa da method assign etmiş oluyoruz.
gelen mesajı konsola yazdırıp diğer peerlara dağıtan örnek bir fonksiyon implemente edelim
/**
* @string {server_ip}: set by system
* @string {node_ip}
* @int {port}
* @double {version}
* @string {api_path}
*/
public JToken sayHello(JToken parameters)
{
string? serverIp = (string)parameters["server_ip"] ?? null;
if(isSenderBanned(serverIp))
{
throw new AuthenticationException("Sender peer is banner from network");
}
if(isSenderSelf(serverIp))
{
return new JObject
{
["result"] = "OK, but process killed for avoiding deadlock",
};
}
Console.WriteLine("HELLO !");
executeHelloBroadcast($"hello from {serverIp}");
JObject response = new JObject
{
["result"] = "OK",
};
return response;
}
private void executeHelloBroadcast(string message)
{
List<string> peerEndpointList = new List<string>();
for(int i = 0; i < peerRepository.Count; i++)
{
PeerModel eachPeer = peerRepository[i];
if(!eachPeer.trust)
continue;
string eachEndpoint = eachPeer.GetEndpoint();
if(eachEndpoint != "" || eachEndpoint != null)
{
peerEndpointList.Add(eachPeer.GetEndpoint());
}
}
JObject responseData = new JObject
{
["message"] = message
};
RpcRequest.SendBroadcast(peerEndpointList, "sayHello", responseData);
}
private bool isSenderBanned(string? senderIp)
{
if(senderIp == null)
{
return false;
}
for(int i = 0; i < peerRepository.Count; i++)
{
PeerModel eachPeer = peerRepository[i];
if(eachPeer.ipAddress == senderIp && !eachPeer.trust)
{
return true;
}
}
return false;
}
private bool isSenderSelf(string? senderIp)
{
if(senderIp == null)
{
return true;
}
List<string> preventList = new List<string>
{
"localhost",
"127.0.0.1",
"::1",
Config.RuntimeConfig.ServerIp
};
return preventList.Contains(senderIp);
}
Burada eğer bize jsonRpc gönderen peer; kendi Ip adresimiz değilse veya blokladığımız bir ip adresine sahip değilse mesajını konsola yazdırıp ağda yayılmasını sağlıyoruz.
Ağda yayılması için geliştirdiğimiz executeHelloBroadcast fonksiyonu ise görebileceğin üzere, peerRepository’mizde iletişime uygun olan peer adreslerini toplayıp, daha önce rpcRequest sınıfında implemente ettiğimiz SendBroadcast fonksiyonunu kullanarak ağda yayılmasını sağlamaktadır.
— — — — —
Artık biraz daha ileri seviyeli bir implementasyon deneyebiliriz. Senaryomuz şu şekilde olacak;
- Bir peer (Node) kendini veya başka bir peer’i tanıtmak için jsonRpc ile registerNewNode isteği gönderir.
- Eğer bu peer bizim repository mizde zaten varsa veya bloklanmış bir peer ise işlemi reddederiz.
- Eğer madde 2 deki şartlarda bir sorun yok ise yeni peer’in bilgilerini kendi repository mize ekleriz ve depolamamıza da kaydederiz.
- Ayrıca kendi repository mizdeki tüm peerlara aynı isteği iletiriz.
- İşlem bu şekilde devam eder. Gönderdiğimiz peer’lar bize aynı isteği geri gönderdiğinde, zaten repository mizde mevcut olduğu için yayılım durur. Böylece sürekli yayılım hali ve kaynak kilitlenmesi yaşanmaz.
/**
* @string {server_ip}: set by system
* @string {node_ip}
* @int {port}
* @double {version}
* @string {api_path}
*/
public JToken registerNewNode(JToken parameters)
{
string? serverIp = (string)parameters["server_ip"] ?? null;
string? nodeIp = (string)parameters["node_ip"] ?? null;
int port = (int)parameters["port"];
double version = (double)parameters["version"];
string? apiPath = (string)parameters["api_path"] ?? null;
if(serverIp == null || nodeIp == null || port == 0 || version == 0 || apiPath == null)
{
throw new WarningException("parameter mismatch");
}
if(isSenderBanned(serverIp))
{
throw new AuthenticationException("Sender peer is banner from network");
}
if(isPeerExist(nodeIp))
{
throw new AuthenticationException("Peer IP is already exist in network");
}
executeNewNodeBroadcast(nodeIp, port, version, apiPath);
addToPeerRepository(nodeIp, port, version, apiPath);
dumpPeerRepository();
JObject response = new JObject
{
["result"] = "OK"
};
return response;
}
private bool isPeerExist(string serverIp)
{
for(int i = 0; i < peerRepository.Count; i++)
{
PeerModel eachPeer = peerRepository[i];
if(eachPeer.ipAddress == serverIp)
{
return true;
}
}
return false;
}
Not: Bu işlem, sonsuz veri tekrarına yol açmamalıdır. Bunun için Flooding, Spanning Trees, Ripple Forwarding gibi ağ yayılım algoritmaları tasarlanmıştır. Bunları kullanabilir veya bu yazıda yaptığımız gibi, kendi çözümünüzü geliştirebilirsiniz.
— — — — —
Bildiğiniz üzere proje kapsamında ilişkisel bir veritabanı kullanmıyoruz. Dolayısıyla verilerimizi sıcak olarak in-memory biçimde tutacağız. Aynı zamanda, her değişiklikte diske de yazacağız.
Bunun için utility içerisinde yer alan Storage sınıfını kullanacağız;
// Write all peers to file.
// Do not write peers that already existing in config.yaml
private void dumpPeerRepository()
{
List<PeerModel> cleanedPeerRepository = new List<PeerModel>();
peerRepository.ForEach(eachPeer => {
if(!Config.RuntimeConfig.PeerEndpoints.Contains(eachPeer.GetEndpoint()))
{
cleanedPeerRepository.Add(eachPeer);
}
});
Storage.WriteToJsonFile(peerPath, cleanedPeerRepository);
}
private List<PeerModel> loadPeerRepository()
{
List<PeerModel> loadedPeerList = Storage.LoadFromJsonFile<List<PeerModel>>(peerPath) ?? new List<PeerModel>();
return loadedPeerList;
}
private List<PeerModel> loadPeerRepositoryFromConfig(List<string> peerEndpointList)
{
List<PeerModel> peerList = new List<PeerModel>();
for(int i = 0; i < peerEndpointList.Count; i++)
{
PeerModel newPeer = new PeerModel(peerEndpointList[i]);
if(newPeer.ipAddress != null)
{
peerList.Add(newPeer);
}
}
return peerList;
}
— — — — —
Not: Ağınızı tüm internetten erişilebilir biçimde açmak için sabit IP sahibi bir sunucu üzerinde Node yazılımınızı çalıştırmalısınız.
Basit bir test yapmak için sunucumuzu
dotnet run
komutu ile ayağa kaldıralım ve Postman ile bir sayHello jsonRpc isteği gönderelim:
Projenin tüm kodlarına bu linkten ulaşabilirsiniz.
8. Tamam, peki neden bir başkası bizim kurduğumuz ağda Node sunucusu çalıştırsın ki ?
Ağdaki Node’lar hatırlarsanız işlemlerden ve ağdaki verinin bir kayıt defteri halinde yedeğinin tutulmasından sorumlu idi. Eğer ağdaki verileri çekmek veya yeni blok eklemek isterseniz, sizin de ağda Node çalıştırmanız gerekir.
Önemli Not: Günümüzde yaygın ağlarda bu işlemler için (Örn Ethereum ağı) sıfırdan Node sunucusu kurmanıza gerek yoktur. Bu yeni başlayanlar için hem kafa karıştırıcı hem de zahmetli bir iştir. Bunun yerine, ağda birçok Node’u bulunan ve size belirli bir ücretlendirme politikası ile ağdaki verilere erişim sağlayan 3.cü parti şirketler bulunmaktadır. (Alchemy, Infura vs.)
Ayrıca tek sebep bu değildir. Örneğin bir full node verileri çekmenin yanı sıra verilere ekleme yapmaktan da sorumludur. Blockchain ekosisteminde verilere ekleme yapma konusunu ileriki yazılarımızda “Mining” olarak işleyeceğiz. Yani tek kazanç verilere erişmek değildir. Ağlar, node çalıştırdığınız için bir bakıma sizi ödüllendirir.
Sistem daima ağda çalışan nodeları ödüllendirecek şekilde kurgulanmalıdır ki ağa katılan node sayısı böylece günden güne artacak; ağ kapasitesi ve transaction uptime gelişme gösterecektir.
9. Final
Peki, merkeziyetsiz ağ demek aslında birden fazla merkezi olan ve belirli tarafsız şartlar dahilinde her adayın merkez olabileceği ağ demek ise, bu merkezlerin arasındaki fikir birliğini nasıl sağlayabiliriz ?
İşte bunun için güvenle tutacağımız bir defter yapısına ve bu merkezlerin birbiri arasında yaptıkları veri transferini matematiksel teknikler vasıtası ile doğrulamasını sağlayan Consensus algoritmalarına ihtiyacımız var.
Consensus Algoritmaları sayesinde ağdaki bir Node’un diğer Node’lara gönderdiği verilerin matematiksel olarak doğrulanması ve güven sorununun çözülmesini sağlamaktadır.
Böylelikle çok merkezli veri işleyen bir ağda veri güvenliği herhangi bir merkezin inisiyatifine bırakılmaz. Bunun yerine veri güvenliği; bilgisayar bilimi, matematik ve kriptolojinin güvenli ellerine emanet edilir.
Önümüzdeki yazıda bir blockchain mekanizması oluşturacağız ve consensus algoritmalarından detaylıca bahsedeceğiz. Şimdilik görüşmek üzere :)