Blokķēde: ko mums maksā PoC uzbūvēt?

Acis baidās, bet rokas niez!

Iepriekšējās rakstos mēs izpētījām tehnoloģijas, uz kurām balstās blokķēdes (Ko mums izmaksā blockchain radīt?) un gadījumus, kurus iespējams īstenot ar to palīdzību (Kas mums jādara, lai uzbūvētu gadījumu?). Ir pienācis laiks strādāt ar rokām! Lai realizētu pilotprojektus un PoC (Proof of Concept), es dodu priekšroku izmantot mākoni, jo pie tam ir pieejams no jebkuras vietas pasaulē, un bieži vien nav jātērē laiks garlaicīgai vides uzstādīšanai, jo ir iepriekš konfigurētas sistēmas. Tātad, apskatīsim kaut ko vienkāršu, piemēram, tīklu monētu pārsūtīšanai starp dalībniekiem, ko nosauksim pieticīgi par Citoins. Šim nolūkam izmantosim IBM mākoni un universālo blokķēdi Hyperledger Fabric. Vispirms izpētīsim, kāpēc Hyperledger Fabric dēvē par universālo blokķēdi?

Blokķēde: ko mums maksā PoC uzbūvēt?

Hyperledger Fabric — universāla blokķēde

Ja runājam vispārīgi, tad universāla informācijas sistēma ir:

  • Serveru kopums un programmatūras kodols, kas izpilda biznesa loģiku;
  • Interfeisi mijiedarbībai ar sistēmu;
  • Rīki ierīču / cilvēku reģistrēšanai, autentificēšanai un autorizēšanai;
  • Datu bāze, kas glabā operatīvos un arhivētos datus:

Blokķēde: ko mums maksā PoC uzbūvēt?

Oficiālo versiju par to, kas ir Hyperledger Fabric, var izlasīt vietnē, bet īsumā, Hyperledger Fabric — ir opensource platforma, kas ļauj veidot slēgtas blokķēdes un veikt jebkādus viedos līgumus, kas uzrakstīti programmēšanas valodās JS un Go. Apskatīsim detalizēti Hyperledger Fabric arhitektūru un pārliecināsimies, ka tā ir universāla sistēma, kurā pastāv specifika datu glabāšanā un ierakstē. Šī specifika ir tāda, ka dati, kā visās blokķēdēs, tiek glabāti blokos, kuri tiek ievietoti blokķēdē tikai tad, ja dalībnieki ir nonākuši pie konsensa, un pēc ierakstīšanas datus nav iespējams nemanāmi labot vai dzēst.

Hyperledger Fabric arhitektūra

Shēmā attēlota Hyperledger Fabric arhitektūra:

Blokķēde: ko mums maksā PoC uzbūvēt?

Organizācijas — organizācijas satur peer’us, tādējādi blokķēde pastāv, pateicoties organizāciju atbalstam. Atšķirīgas organizācijas var pievienoties vienam kanālam.

Channel — loģiskā struktūra, kas apvieno peer’us grupās, tādējādi nosakot blokķēdi. Hyperledger Fabric var vienlaikus apstrādāt vairākas blokķēdes ar atšķirīgām biznesa loģikām.

Membership Services Provider (MSP) — tas ir CA (Certificate Authority), kas izsniedz identitātes un piešķir lomas. Lai izveidotu mezglu, jāsadarbojas ar MSP.

Peer mezgli — pārbauda darījumus, glabā blokķēdi, izpilda viedos līgumus un mijiedarbojas ar lietotnēm. Peer-iem ir identitāte (digitālais sertifikāts), ko izdod MSP. Atšķirībā no Bitcoin vai Etherium tīkla, kur visas mezgls ir līdzvērtīgi, Hyperledger Fabric mezgli ieņem dažādas lomas:

  • Peer var būt apstiprinošais peer (EP) un izpildīt viedos līgumus.
  • Apstiprinošais peer (CP) — tikai glabā datus blokķēdē un aktualizē „World state”.
  • Ankura Peer (AP) — ja blokķēdē piedalās vairākas organizācijas, tad ankoru peer-iem izmanto, lai sazinātos starp tām. Katrai organizācijai jābūt vienam vai vairākiem ankoru peer. Ar AP jebkurš peer organizācijā var saņemt informāciju par visiem peer-iem citās organizācijās. Lai sinhronizētu informāciju starp AP, tiek izmantots gossipa protokols.
  • Līdera Peer — ja organizācijai ir vairāki peer-iem, tad tikai līdera peer saņems blokos no Ordering service un nodos tos pārējiem peer-iem. Līderi var noteikt gan statiski, gan izvēlēties dinamiski organizācijas peer-iem. Lai sinhronizētu informāciju par līderiem, arī tiek izmantots gossipa protokols.

Aktīvi — vienības, kas ir vērtīgas un tiek glabātas blokķēdē. Precīzāk — tās ir key-value dati JSON formātā. Tieši šie dati tiek ierakstīti blokķēdē „Blockchain”. Tiem ir vēsture, kas tiek glabāta blokķēdē, un pašreizējais stāvoklis, kas tiek glabāts „World state” datu bāzē. Datu struktūras piepildās brīvi atkarībā no biznesa uzdevumiem. Nav nekādu obligātu lauku, vienīgā rekomendācija — aktīviem jābūt īpašniekam un jābūt vērtīgām.

Grāmata — sastāv no blokķēdes „Blockchain” un datu bāzes „World state”, kurā tiek glabāts pašreizējais aktīvu stāvoklis. World state izmanto LevelDB vai CouchDB.

Viedais līgums — ar viedajiem līgumiem tiek īstenota sistēmas biznesa loģika. Hyperledger Fabric viedie līgumi tiek saukti par chaincode. Ar chaincode tiek noteikti aktīvi un darījumi ar tiem. Tehniskā valodā sakot, viedie līgumi ir programmatūras moduļi, kas izstrādāti JS vai Go programmēšanas valodās.

Apstiprināšanas politika — katram chaincode var noteikt politikas, cik daudz un no kā nepieciešams sagaidīt apstiprinājumus darījumam. Ja politika nav noteikta, tad pēc noklusējuma tiek izmantots: “darījumu jāapstiprina jebkuram dalībniekam (member) no jebkuras organizācijas kanālā”. Politiku piemēri:

  • Transakciju jāapstiprina jebkuram organizācijas administratoram;
  • To var apstiprināt jebkurš organizācijas loceklis vai klients;
  • To jāapstiprina jebkuram organizācijas peer.

Pasūtīšanas pakalpojums — iesaiņo transakcijas blokos un nosūta peer am kanālā. Garantē ziņojumu piegādi visiem peer am tīklā. Rūpnieciskajām sistēmām tiek izmantots ziņojumu brokeris Kafka, izstrādei un testēšanai Solo.

CallFlow

Blokķēde: ko mums maksā PoC uzbūvēt?

  • Lietotne mijiedarbojas ar Hyperledger Fabric, izmantojot Go, Node.js vai Java SDK;
  • Klients izveido transakciju tx un nosūta to uz apstiprinājuma peer am;
  • Peer pārbauda klienta parakstu, veic transakciju un nosūta apstiprinājuma parakstu atpakaļ klientam. Chaincode tiek izpildīta tikai apstiprinājuma peer, un izpildes rezultāts tiek izplatīts visiem peer am. Šis darba algoritms tiek saukts par — PBFT (Practical Byzantine Fault Tolerant) konsensu. Tas atšķiras no klasiskā BFT tas, ka ziņojums tiek izplatīts un tiek gaidīts apstiprinājums nevis no visiem dalībniekiem, bet tikai no noteikta kopuma;
  • Pēc tam, kad klients saņēmis atbildes skaitu, kas atbilst apstiprinājuma politikai, viņš nosūta transakciju uz Pasūtīšanas pakalpojumu;
  • Pasūtīšanas pakalpojums veido bloku un nosūta to visiem apstiprināšanas peer am. Pasūtīšanas pakalpojums nodrošina secīgu bloku ierakstu, tādējādi izslēdzot tā dēvēto, ledger fork (skatīt sadaļu «Forks»);
  • Peer saņem bloku, vēlreiz pārbauda apstiprinājuma politiku, ieraksta bloku blokķēdē un maina stāvokli «World state» DB.

T.i., tiek izveidota lomu sadalīšana starp mezgliem. Tas nodrošina blokķēdes mērogojamību un drošību:

  • Viedie līgumi (chaincode) tiek izpildīti apstiprinājuma peer. Tas nodrošina smarta kontraktu konfidencialitāti, jo tie netiek glabāti pie visiem dalībniekiem, bet tikai apstiprinājuma peer os.
  • Pasūtījumam jāstrādā ātri. To nodrošina tas, ka Pasūtīšana tikai veido bloku un nosūta to noteiktai grupai līdera peer.
  • Apstiprinājuma peer tikai glabā blokķēdi — to var būt daudz un tiem nav nepieciešama liela jauda vai nekavējoša darbība.

Detalizētāk ar arhitektūras risinājumiem Hyperledger Fabric un kāpēc tas darbojas tā, nevis citādi var apskatīt šeit: Architecture Origins vai šeit: Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains.

Tātad, Hyperledger Fabric ir patiesi universāla sistēma, ar kuru var:

  • Realizēt pēc pasūtījuma izstrādātu biznesa loģiku, izmantojot viedlīgumu mehānismu;
  • Reģistrēt un iegūt datus no blokķēdes datubāzes JSON formātā;
  • Nodrošināt un pārbaudīt piekļuvi API, izmantojot Sertifikācijas iestādi.

Tagad, kad esam nedaudz iepazinušies ar Hyperledger Fabric specifiku, sāksim beidzot kaut ko noderīgu!

Izveidojam blokķēdi

Uzdevuma formulēšana

Uzdevums ir īstenot Citcoin tīklu ar šādām funkcijām: izveidot kontu, iegūt bilanci, papildināt kontu, pārsūtīt monētas no vienas konta uz citu. Uzzīmēsim objektu modeli, ko vēlāk īstenosim viedo līgumu. Tātad mums būs konti, kas tiek identificēti ar nosaukumiem (name) un satur bilanci (balance), un kontu saraksts. Konti un kontu saraksts — Hyperledger Fabric terminoloģijā ir aktīvi (assets). Tādejādi tiem ir vēsture un pašreizējais stāvoklis. Mēģināšu to vizuāli attēlot:

Blokķēde: ko mums maksā PoC uzbūvēt?

Augšējās figūras attēlo pašreizējo stāvokli, kas tiek glabāts „World state”. Zem tām ir figūras, kas parāda vēsturi, kas tiek glabāta blokķēdē. Aktīvu pašreizējais stāvoklis tiek mainīts ar darījumiem. Aktīvs tiek mainīts tikai kopumā, tādēļ transaction veikšanas rezultātā tiek izveidots jauns objekts, un pašreizējā aktīva vērtība nonāk vēsturē.

IBM mākoņpakalpojums

Izveidojam kontu IBM mākoņpakalpojumā. Lai izmantotu blokķēdes platformu, tai ir jāveic atjauninājums uz Pay-As-You-Go. Šis process var būt lēns, jo IBM pieprasa papildu informāciju un to pārbauda manuāli. No pozitīvās puses, varu teikt, ka IBM ir labi mācību materiāli, kas ļauj izveidot Hyperledger Fabric savā mākoņpakalpojumā. Man patika šis rakstu un piemēru cikls:

Tālāk ir sniegtas IBM Blokķēdes platformas ekrānuzņēmumi. Tas nav rokasgrāmata blokķēdes izveidošanai, bet tikai uzdevuma apjoma demonstrācija. Tātad mūsu mērķiem izveidojam vienu organizāciju:

Blokķēde: ko mums maksā PoC uzbūvēt?

Izveidojam mezglus: Orderer CA, Org1 CA, Orderer Peer:

Blokķēde: ko mums maksā PoC uzbūvēt?

Izveidojam lietotājus:

Blokķēde: ko mums maksā PoC uzbūvēt?

Izveidojam kanālu un nosaucam to par citcoin:

Blokķēde: ko mums maksā PoC uzbūvēt?

Patiess kanāls ir blokķēde, tāpēc tas sākas ar nulles bloku (Genesis block):

Blokķēde: ko mums maksā PoC uzbūvēt?

Rakstām viedo līgumu

/*
 * Citcoin smart-contract v1.5 for Hyperledger Fabric
 * (c) Alexey Sushkov, 2019
 */
 
'use strict';
 
const { Contract } = require('fabric-contract-api');
const maxAccounts = 5;
 
class CitcoinEvents extends Contract {
 
    async instantiate(ctx) {
        console.info('instantiate');
        let emptyList = [];
        await ctx.stub.putState('accounts', Buffer.from(JSON.stringify(emptyList)));
    }
    // Get all accounts
    async GetAccounts(ctx) {
        // Get account list:
        let accounts = '{}'
        let accountsData = await ctx.stub.getState('accounts');
        if (accountsData) {
            accounts = JSON.parse(accountsData.toString());
        } else {
            throw new Error('accounts not found');
        }
        return accountsData.toString()
    }
     // add a account object to the blockchain state identifited by their name
    async AddAccount(ctx, name, balance) {
        // this is account data:
        let account = {
            name: name,
            balance: Number(balance),       
            type: 'account',
        };
        // create account:
        await ctx.stub.putState(name, Buffer.from(JSON.stringify(account)));
 
        // Add account to list:
        let accountsData = await ctx.stub.getState('accounts');
        if (accountsData) {
            let accounts = JSON.parse(accountsData.toString());
            if (accounts.length < maxAccounts)
            {
                accounts.push(name);
                await ctx.stub.putState('accounts', Buffer.from(JSON.stringify(accounts)));
            } else {
                throw new Error('Max accounts number reached');
            }
        } else {
            throw new Error('accounts not found');
        }
        // return  object
        return JSON.stringify(account);
    }
    // Sends money from Account to Account
    async SendFrom(ctx, fromAccount, toAccount, value) {
        // get Account from
        let fromData = await ctx.stub.getState(fromAccount);
        let from;
        if (fromData) {
            from = JSON.parse(fromData.toString());
            if (from.type !== 'account') {
                throw new Error('wrong from type');
            }   
        } else {
            throw new Error('Accout from not found');
        }
        // get Account to
        let toData = await ctx.stub.getState(toAccount);
        let to;
        if (toData) {
            to = JSON.parse(toData.toString());
            if (to.type !== 'account') {
                throw new Error('wrong to type');
            }  
        } else {
            throw new Error('Accout to not found');
        }
 
        // update the balances
        if ((from.balance - Number(value)) >= 0 ) {
            from.balance -= Number(value);
            to.balance += Number(value);
        } else {
            throw new Error('From Account: not enought balance');          
        }
 
        await ctx.stub.putState(from.name, Buffer.from(JSON.stringify(from)));
        await ctx.stub.putState(to.name, Buffer.from(JSON.stringify(to)));
                 
        // define and set Event
        let Event = {
            type: "SendFrom",
            from: from.name,
            to: to.name,
            balanceFrom: from.balance,
            balanceTo: to.balance,
            value: value
        };
        await ctx.stub.setEvent('SendFrom', Buffer.from(JSON.stringify(Event)));
 
        // return to object
        return JSON.stringify(from);
    }
 
    // get the state from key
    async GetState(ctx, key) {
        let data = await ctx.stub.getState(key);
        let jsonData = JSON.parse(data.toString());
        return JSON.stringify(jsonData);
    }
    // GetBalance   
    async GetBalance(ctx, accountName) {
        let data = await ctx.stub.getState(accountName);
        let jsonData = JSON.parse(data.toString());
        return JSON.stringify(jsonData);
    }
     
    // Refill own balance
    async RefillBalance(ctx, toAccount, value) {
        // get Account to
        let toData = await ctx.stub.getState(toAccount);
        let to;
        if (toData) {
            to = JSON.parse(toData.toString());
            if (to.type !== 'account') {
                throw new Error('wrong to type');
            }  
        } else {
            throw new Error('Accout to not found');
        }
 
        // update the balance
        to.balance += Number(value);
        await ctx.stub.putState(to.name, Buffer.from(JSON.stringify(to)));
                 
        // define and set Event
        let Event = {
            type: "RefillBalance",
            to: to.name,
            balanceTo: to.balance,
            value: value
        };
        await ctx.stub.setEvent('RefillBalance', Buffer.from(JSON.stringify(Event)));
 
        // return to object
        return JSON.stringify(from);
    }
}
module.exports = CitcoinEvents;

Cik intuitīvi tas jābūt skaidram:

  • Pastāv vairākas funkcijas (AddAccount, GetAccounts, SendFrom, GetBalance, RefillBalance), kuras demo programma izsauks, izmantojot Hyperledger Fabric API.
  • Funkcijas SendFrom un RefillBalance ģenerē notikumus (Event), kurus demo programma saņems.
  • Instantiēšanas funkcija tiek izsaukta vienu reizi, kad tiek izveidots viedais līgums. Patiesībā to izsauc nevis vienu reizi, bet katru reizi, kad tiek mainīta viedā līguma versija. Tādēļ saraksta inicializēšana ar tukšu masīvu ir slikta ideja, jo tagad, mainot viedā līguma versiju, mēs pazaudēsim esošo sarakstu. Bet nekas, es tikai mācos).
  • Accounti un accountu saraksts (accounts) ir JSON datu struktūras. Datu manipulācijai tiek izmantots JS.
  • Iegūt pašreizējo asset vērtību var ar getState funkcijas izsaukumu, bet atjaunināt to ar putState.
  • Izveidei Account tiek izsaukta AddAccount funkcija, kurā tiek salīdzināts maksimālais account skaits blokķēdē (maxAccounts = 5). Un šeit ir kļūda (pamanījāt?), kas noved pie bezgalīga account skaita pieauguma. Šādām kļūdām jāizvairās)

Tālāk mēs augšupielādējam viedo līgumu kanālā un to instancējam:

Blokķēde: ko mums maksā PoC uzbūvēt?

Skatāmies darījumu viedā līguma uzstādīšanai:

Blokķēde: ko mums maksā PoC uzbūvēt?

Skatāmies detaļas par mūsu kanālu:

Blokķēde: ko mums maksā PoC uzbūvēt?

Rezultātā iegūstam šādu blokķēdes tīkla shēmu IBM mākoņa vidē. Shēmā ir arī demo programma, kas darbojas Amazon mākoņī uz virtuālā servera (par to sīkāk nākamajā sadaļā):

Blokķēde: ko mums maksā PoC uzbūvēt?

GUI izveide Hyperledger Fabric API izsaukumiem

Hyperledger Fabric ir API, ko var izmantot:

  • Lai izveidotu kanālu;
  • Lai pieslēgtu peer kanālam;
  • Lai instalētu un instancētu viedos līgumus kanālā;
  • Lai izsauktu darījumus;
  • Lai pieprasītu informāciju blokķēdē.

Pieteikuma izstrāde

Mūsu demo programmā mēs izmantosim API tikai darījumu izsaukšanai un informācijas pieprasīšanai, jo citus soļus mēs jau esam veikuši, izmantojot IBM blokķēdes platformu. Rakstīsim GUI, izmantojot standarta tehnoloģiju kaudzi: Express.js + Vue.js + Node.js. Par to, kā uzsākt mūsdienīgu tīmekļa pieteikumu izveidi, varētu uzrakstīt atsevišķu rakstu. Šeit atstāšu saiti uz lekciju sēriju, kas man visvairāk patika: Full Stack Web App using Vue.js & Express.js. Rezultātā izveidojām klient-server lietojumprogrammu ar pazīstamu grafisko saskarni, kas atbilst Google Materiāla dizainam. REST API starp klientu un serveri sastāv no vairākiem izsaukumiem:

  • HyperledgerDemo/v1/init — inicializēt blokķēdi;
  • HyperledgerDemo/v1/accounts/list — iegūt visu accountu sarakstu;
  • HyperledgerDemo/v1/account?name=Bob&balance=100 — izveidot Bob account;
  • HyperledgerDemo/v1/info?account=Bob — iegūt informāciju par Bob account;
  • HyperledgerDemo/v1/transaction?from=Bob&to=Alice&volume=2 — pārsūtīt divas monētas no Bob uz Alice;
  • HyperledgerDemo/v1/disconnect — pārtraukt savienojumu ar blokķēdi.

API apraksts ar piemēriem ir pieejams vietnē «Postman» — plaši pazīstamajā programmā HTTP API testēšanai.

Demonstrācijas lietojumprogramma Amazon mākoņī

Lietojumprogrammu augšupielādēju Amazon, jo IBM joprojām nav varējusi atjaunināt manu kontu un ļaut izveidot virtuālos serverus. Kā vīksnu pievienoju domēnu: www.citcoin.info. Atstāsšu serveri uz nedaudz laika ieslēgtu, pēc tam izslēgšu, jo nomas centi krājas, bet citcoin monētas biržā vēl netiek kotētas) Ierakstā ievietošu ekrānuzņēmumus no demonstrācijas, lai parādītu darba loģiku. Demonstrācijas lietojumprogramma var:

  • Iesākt blokķēdi;
  • Izveidot kontu (bet šobrīd jaunu kontu nevar izveidot, jo blokķēdē ir sasniegts maksimālais kontu skaits, kas noteikts viedajā kontraktā);
  • Iegūt visu kontu sarakstu;
  • Pārsūtīt citcoin monētas starp Alice, Bob un Alex;
  • Saņemt notikumus (bet šobrīd notikumus nav iespējams parādīt, tāpēc interfeisā vienkāršības labad ir norādīts, ka notikumi netiek atbalstīti);
  • Žurnālu darbības.

Vispirms mēs iesākat blokķēdi:

Blokķēde: ko mums maksā PoC uzbūvēt?

Pēc tam izveidojam savu kontu, nekaunējamies no bilances:

Blokķēde: ko mums maksā PoC uzbūvēt?

Iegūstam visu pieejamo kontu sarakstu:

Blokķēde: ko mums maksā PoC uzbūvēt?

Izvēlamies sūtītāju un saņēmēju, iegūstam viņu bilances. Ja sūtītājs un saņēmējs ir viens un tas pats, tad tiks papildināta viņa konta bilance:

Blokķēde: ko mums maksā PoC uzbūvēt?

Žurnālā sekojam līdzi darījumu izpildei:

Blokķēde: ko mums maksā PoC uzbūvēt?

Par pašu demonstrācijas programmu ir viss. Tālāk varam apskatīt mūsu darījumu blokķēdē:

Blokķēde: ko mums maksā PoC uzbūvēt?

Un kopējo darījumu sarakstu:

Blokķēde: ko mums maksā PoC uzbūvēt?

Ar to mēs veiksmīgi pabeidzām PoC par Citcoin tīkla izveidi. Ko vēl jādara, lai Citcoin kļūtu par pilnvērtīgu valūtas pārskaitījumu tīklu? Ļoti maz:

  • Account veidošanas posmā ieviest privātā/publiskā atslēgas ģenerēšanu. Privātajai atslēgai jāuzglabājas lietotāja accountā, publiskajai — blokķēdē.
  • Veikt monētu pārskaitījumus, kur identificēšanai tiek izmantota ne vārds, bet publiskā atslēga.
  • Šifrēt darījumus, kas nāk no lietotāja uz serveri, izmantojot viņa privāto atslēgu.

Secinājums

Mēs esam izveidojuši Citcoin tīklu ar funkcijām: pievienot kontu, iegūt bilanci, papildināt savu kontu, pārsūtīt monētas no viena konta uz citu. Tātad, kas mums izmaksāja PoC izveidi?

  • Nepieciešams izpētīt blokķēdi kopumā un Hyperledger Fabric it īpaši;
  • Iemācīties izmantot IBM vai Amazon mākoņus;
  • Iemācīties programmēšanas valodu JS un kādu web ietvaru;
  • Ja kādi dati jāuzglabā nevis blokķēdē, bet atsevišķā datu bāzē, tad iemācīties integrēties, piemēram, ar PostgreSQL;
  • Un pēdējais sarakstā, bet ne pēc nozīmības — bez Linux zināšanām mūsdienu pasaulē nekur nav iespējams!)

Protams, tas nav nekas sarežģīts, bet būs jāstrādā!

Avota kodi GitHub

Avoti novietoti uz GitHub. Īss repo apraksts:
Katalogs «serveris» — Node.js serveris
Katalogs «klients» — Node.js klients
Katalogs «blockchain» (parametru vērtības un atslēgas, protams, nav funkcionējošas un ir norādītas tikai piemēram):

  • contract — viedā līguma avots
  • wallet — lietotāja atslēgas Hyperledger Fabric API izmantošanai.
  • *.cds — kompilētās viedlīgumu versijas
  • *.json faili — piemēru konfigurācijas faili Hyperledger Fabric API izmantošanai

Tas ir tikai sākums!

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster