Ethereum Oracle Sözleşmeleri: Sağlamlık Kodu Özellikleri
Yayınlanan: 2022-03-11Bu üçlü bölümün ilk bölümünde, bize basit bir kahin çifti sözleşmesi veren küçük bir eğitimden geçtik. Kurulum (trüf mantarı ile), kodu derleme, bir test ağına yerleştirme, çalıştırma ve hata ayıklama mekanizmaları ve süreçleri anlatılmış; ancak, kodun birçok ayrıntısı elle dalgalı bir şekilde parlatıldı. Şimdi, söz verdiğimiz gibi, Solidity akıllı sözleşme geliştirmeye özgü ve bu özel sözleşme-kehanet senaryosuna özgü dil özelliklerinden bazılarına bakacağız. Her ayrıntıya özenle bakamıyor olsak da (isterseniz daha sonraki çalışmalarınızda bunu size bırakacağım), kodun en çarpıcı, en ilginç ve en önemli özelliklerine değinmeye çalışacağız.
Bunu kolaylaştırmak için, projenin kendi versiyonunu (varsa) açmanızı veya referans için kodu elinizin altında bulundurmanızı tavsiye ederim.
Bu noktada tam kod burada bulunabilir: https://github.com/jrkosinski/Oracle-example/tree/part2-step1
Ethereum ve Sağlamlık
Solidity, mevcut tek akıllı sözleşme geliştirme dili değil, ancak Ethereum akıllı sözleşmeleri için genel olarak en yaygın ve en popüler olduğunu söylemek yeterince güvenli olduğunu düşünüyorum. Kesinlikle, bu yazının yazıldığı sırada en popüler desteğe ve bilgiye sahip olan kişidir.
Solidity, nesne yönelimli ve Turing-tamamlayıcıdır. Bununla birlikte, akıllı sözleşme programlamasını sıradan "bunu yapalım" korsanlığından oldukça farklı hissettiren yerleşik (ve tamamen kasıtlı) sınırlamalarını hızla anlayacaksınız.
Sağlamlık Sürümü
İşte her Solidity kod şiirinin ilk satırı:
pragma solidity ^0.4.17;
Hala gençliğinde olan Solidity hızla değişip evrimleştiğinden, gördüğünüz sürüm numaraları farklı olacak. Versiyon 0.4.17, örneklerimde kullandığım versiyondur; Bu yayının yayınlandığı tarihteki en son sürüm 0.4.25'tir.
Bunu okuduğunuz şu anda en son sürüm tamamen farklı bir şey olabilir. Şimdi tartışacağımız Solidity için pek çok güzel özellik çalışıyor (veya en azından planlanıyor).
İşte farklı Solidity sürümlerine genel bir bakış.
Profesyonel ipucu: Bir dizi sürüm de belirtebilirsiniz (bunun çok sık yapıldığını görmesem de), şöyle:
pragma solidity >=0.4.16 <0.6.0;
Solidity Programlama Dili Özellikleri
Solidity, çoğu modern programcının aşina olduğu ve farklı ve (en azından benim için) olağandışı olan birçok dil özelliğine sahiptir. C++, Python ve JavaScript'ten ilham aldığı söyleniyor - bunların hepsi bana şahsen tanıdık geliyor ve yine de Solidity bu dillerin herhangi birinden oldukça farklı görünüyor.
Sözleşme
.sol dosyası, kodun temel birimidir. BoxingOracle.sol'de 9. satıra dikkat edin:
contract BoxingOracle is Ownable {
Sınıf, nesne yönelimli dillerde mantığın temel birimi olduğundan, Sözleşme, Solidity'de mantığın temel birimidir. Sözleşmenin Solidity'nin "sınıfı" olduğunu söylemek şimdilik basitleştirmek için yeterlidir (nesne yönelimli programcılar için bu kolay bir adımdır).
Miras
Sağlamlık sözleşmeleri, devralmayı tamamen destekler ve beklediğiniz gibi çalışır; özel sözleşme üyeleri miras alınmaz, oysa korumalı ve kamuya açık olanlar miras alınır. Beklediğiniz gibi aşırı yükleme ve polimorfizm desteklenir.
contract BoxingOracle is Ownable {
Yukarıdaki açıklamada, “is” anahtar kelimesi kalıtımı ifade etmektedir. BoxingOracle, Ownable'dan devralır. Solidity'de çoklu kalıtım da desteklenir. Çoklu kalıtım, virgülle ayrılmış sınıf adları listesiyle belirtilir, örneğin:
contract Child is ParentA, ParentB, ParentC { …
Kalıtım modelinizi yapılandırırken aşırı karmaşık davranmak (bence) iyi bir fikir olmasa da, Elmas Problemi ile ilgili olarak Solidity hakkında ilginç bir makale burada.
numaralar
Numaralandırmalar Solidity'de desteklenir:
enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }
Beklediğiniz gibi (tanıdık dillerden farklı değil), her enum değerine 0 ile başlayan bir tamsayı değeri atanır. Solidity belgelerinde belirtildiği gibi, numaralandırma değerleri tüm tamsayı türlerine dönüştürülebilir (örn., uint, uint16, uint32, vb.), ancak örtük dönüştürmeye izin verilmez. Bu, açıkça kullanılmaları gerektiği anlamına gelir (örneğin, uint'e).
Solidity Docs: Enums Enums Eğitimi
yapılar
Yapılar, numaralandırmalar gibi, kullanıcı tanımlı bir veri türü oluşturmanın başka bir yoludur. Yapılar, tüm C/C++ temel kodlayıcılarına ve benim gibi yaşlı adamlara aşinadır. BoxingOracle.sol'ün 17. satırından bir yapı örneği:
//defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }
Tüm eski C programcılarına not: Solidity'de yapısal "paketleme" bir şeydir, ancak bazı kurallar ve uyarılar vardır. Mutlaka C ile aynı şekilde çalıştığını varsaymayın; Paketlemenin belirli bir durumda size yardımcı olup olmayacağını belirlemek için belgeleri kontrol edin ve durumunuzun farkında olun.
Sağlamlık Yapısı Ambalajı
Oluşturulduktan sonra, kodunuzda yapılar yerel veri türleri olarak ele alınabilir. Yukarıda oluşturulan yapı türünün "örnekleme" sözdizimine bir örnek:
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Solidity'de Veri Tipleri
Bu bizi Solidity'deki veri türlerinin çok temel konusuna getiriyor. Sağlamlık hangi veri türlerini destekler? Sağlamlık statik olarak türetilmiştir ve bu yazı yazılırken veri türleri açıkça bildirilmeli ve değişkenlere bağlanmalıdır.
Sağlamlık Veri Tipleri
Boole değerleri
Boole türleri bool adı altında desteklenir ve true veya false değerleri
sayısal türler
int8/uint8'den int256/uint256'ya (sırasıyla 8-bit tam sayılardan 256-bit tam sayılara kadar) tam sayı türleri hem imzalı hem de imzasız olarak desteklenir. uint türü, uint256'nın kısaltmasıdır (ve aynı şekilde int, int256'nın kısaltmasıdır).
Özellikle, kayan nokta türleri desteklenmez . Neden? Bir şey için, parasal değerlerle uğraşırken, kayan nokta değişkenlerinin kötü bir fikir olduğu iyi bilinir (elbette genel olarak), çünkü değer bir anda kaybolabilir. Eter değerleri, bir eterin 1/1,000,000,000,000,000,000'i olan wei cinsinden belirtilir ve bu, tüm amaçlar için yeterli hassasiyet olmalıdır; bir etheri daha küçük parçalara ayıramazsınız.
Sabit nokta değerleri şu anda kısmen desteklenmektedir. Solidity belgelerine göre: "Sabit nokta sayıları henüz Solidity tarafından tam olarak desteklenmiyor. Bildirilebilirler, ancak atanamazlar veya atanamazlar.”
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Not: Değişkenin boyutunu azaltmak (örneğin uint32'ye) aslında gaz maliyetlerini beklediğiniz gibi azaltmak yerine artırabileceğinden, çoğu durumda yalnızca uint kullanmak en iyisidir. Genel bir kural olarak, aksini yapmak için iyi bir nedeniniz olduğundan emin değilseniz uint kullanın.
Dize Türleri
Solidity'deki string veri tipi komik bir konudur; Kiminle konuştuğunuza bağlı olarak farklı fikirler edinebilirsiniz. Solidity'de bir string veri tipi vardır, bu bir gerçektir. Benim düşüncem, muhtemelen çoğu kişi tarafından paylaşılıyor, fazla işlevsellik sunmuyor. Dize ayrıştırma, birleştirme, değiştirme, kırpma, hatta dizenin uzunluğunu sayma: bir dize türünden beklediğiniz şeylerin hiçbiri mevcut değildir ve bu nedenle (ihtiyacınız varsa) bunlar sizin sorumluluğunuzdadır. Bazı kişiler string yerine bytes32 kullanır; bu da yapılabilir.
Solidity dizileri hakkında eğlenceli makale
Benim fikrim: Kendi string türünüzü yazmak ve genel kullanım için yayınlamak eğlenceli bir alıştırma olabilir.
Adres Tipi
Belki de Solidity'ye özgü, özellikle Ethereum cüzdanı veya sözleşme adresleri için bir adres veri tipine sahibiz. Bu, belirli boyuttaki adresleri depolamak için özel olarak 20 baytlık bir değerdir. Ayrıca, özellikle bu tür adresler için tür üyelerine sahiptir.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Adres Veri Tipleri
TarihSaat Türleri
Solidity'de, örneğin JavaScript'te olduğu gibi, yerel Date veya DateTime türü yoktur. (Oh hayır—Solidity'nin sesi her paragrafta daha da kötüleşiyor!?) Tarihler doğal olarak uint (uint256) türünün zaman damgaları olarak ele alınır. Blok zaman damgası Unix tarzı bir zaman damgası olduğundan, genellikle milisaniye yerine saniye cinsinden Unix tarzı zaman damgaları olarak işlenirler. Çeşitli nedenlerle insan tarafından okunabilir tarihlere ihtiyaç duyduğunuz durumlarda, açık kaynak kitaplıklar mevcuttur. BoxingOracle'da bir tane kullandığımı fark edebilirsiniz: DateLib.sol. OpenZeppelin ayrıca tarih yardımcı programlarına ve diğer birçok genel yardımcı program kitaplığına sahiptir (Kısa süre içinde Solidity'nin kitaplık özelliğine geleceğiz).
Profesyonel ipucu: OpenZeppelin, sözleşmelerinizi oluşturmanıza yardımcı olabilecek hem bilgi hem de önceden yazılmış genel kod için iyi bir kaynaktır (ancak elbette tek iyi kaynak değildir).
Eşlemeler
BoxingOracle.sol'ün 11. satırının eşleme adı verilen bir şeyi tanımladığına dikkat edin:
mapping(bytes32 => uint) matchIdToIndex;
Solidity'deki bir eşleme, hızlı aramalar için özel bir veri türüdür; esasen bir arama tablosu veya bir karma tabloya benzer, burada bulunan veriler blok zincirinin kendisinde yaşar (eşleme tanımlandığında, burada olduğu gibi, bir sınıf üyesi olarak). Sözleşmenin yürütülmesi sırasında, bir karma tabloya veri eklemeye benzer şekilde eşlemeye veri ekleyebilir ve daha sonra eklediğimiz değerleri arayabiliriz. Bu durumda eklediğimiz verilerin blok zincirinin kendisine eklendiğini ve böylece devam edeceğini tekrar unutmayın. Bugün New York'taki haritaya eklersek, bundan bir hafta sonra İstanbul'da birileri okuyabilir.
BoxingOracle.sol'ün 71. satırından eşlemeye ekleme örneği:
matchIdToIndex[id] = newIndex+1
BoxingOracle.sol'ün 51. satırındaki eşlemeden okuma örneği:
uint index = matchIdToIndex[_matchId];
Öğeler eşlemeden de kaldırılabilir. Bu projede kullanılmıyor, ancak şöyle görünecek:
delete matchIdToIndex[_matchId];
Dönüş Değerleri
Fark etmiş olabileceğiniz gibi, Solidity, Javascript'e biraz yüzeysel bir benzerlik gösterebilir, ancak JavaScript'in tür ve tanım gevşekliğinin çoğunu devralmaz. Bir sözleşme kodu oldukça katı ve kısıtlı bir şekilde tanımlanmalıdır (ve kullanım durumu düşünüldüğünde bu muhtemelen iyi bir şeydir). Bunu akılda tutarak, BoxingOracle.sol'ün 40. satırındaki işlev tanımını göz önünde bulundurun.
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
Tamam, öyleyse, önce burada nelerin bulunduğuna hızlı bir genel bakış yapalım. function
onu bir işlev olarak işaretler. _getMatchIndex
işlevin adıdır (alt çizgi özel üyeyi belirten bir kuraldır - bunu daha sonra tartışacağız). _matchId
(bu sefer alt çizgi kuralı işlev bağımsız değişkenlerini belirtmek için kullanılır) adlı bir bağımsız değişken bytes32
. private
anahtar sözcüğü aslında üyeyi kapsamda özel yapar, view
derleyiciye bu işlevin blok zincirindeki herhangi bir veriyi değiştirmediğini söyler ve son olarak: ~~~ sağlamlık döndürür (uint) ~~~
Bu, işlevin bir uint döndürdüğünü söyler (void döndüren bir işlevin burada hiçbir returns
yan tümcesi olmaz). uint neden parantez içinde? Bunun nedeni, Solidity işlevlerinin tuple'ları döndürebilmesi ve sıklıkla yapmasıdır.
Şimdi, 166. satırdaki aşağıdaki tanımı düşünün:
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Bu konudaki iade maddesine göz atın! Bir, iki… yedi farklı şey döndürür. Tamam, öyleyse, bu işlev bunları bir demet olarak döndürür. Niye ya? Geliştirme sırasında, genellikle kendinizi bir yapı döndürmeye ihtiyaç duyarsınız (eğer JavaScript olsaydı, muhtemelen bir JSON nesnesi döndürmek isterdiniz). Pekala, bu yazı itibariyle (gelecekte bu değişebilir), Solidity, yapıların genel işlevlerden döndürülmesini desteklemiyor. Yani bunun yerine tuples döndürmeniz gerekiyor. Bir Python adamıysanız, tuple'larla zaten rahat olabilirsiniz. Pek çok dil, en azından bu şekilde olmasa da, onları gerçekten desteklemiyor.
Tuple'ı dönüş değeri olarak döndürme örneği için 159. satıra bakın:
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
Ve böyle bir şeyin dönüş değerini nasıl kabul ederiz? Şöyle yapabiliriz:
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Alternatif olarak, değişkenleri doğru türleriyle önceden açıkça bildirebilirsiniz:
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Ve şimdi kullanabileceğimiz 7 dönüş değerini tutmak için 7 değişken bildirdik. Aksi takdirde, değerlerden sadece bir veya ikisini istediğimizi varsayarsak, şunu söyleyebiliriz:
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Orada ne yaptığımızı gördünüz mü? Sadece ilgilendiğimiz ikisini aldık. Şu virgüllerin tümüne bir göz atın. Onları dikkatlice saymalıyız!
ithalat
BoxingOracle.sol'ün 3. ve 4. satırları içe aktarılır:
import "./Ownable.sol"; import "./DateLib.sol";
Muhtemelen beklediğiniz gibi, bunlar BoxingOracle.sol ile aynı sözleşmeler proje klasöründe bulunan kod dosyalarından tanımları içe aktarıyor.
değiştiriciler
İşlev tanımlarının bir dizi değiştirici eklenmiş olduğuna dikkat edin. İlk olarak, görünürlük vardır: özel, genel, dahili ve harici—işlev görünürlüğü.
Ayrıca, pure
anahtar kelimeleri ve view
göreceksiniz. Bunlar, derleyiciye, varsa işlevin ne tür değişiklikler yapacağını gösterir. Bu önemlidir, çünkü böyle bir şey, işlevi çalıştırmanın nihai gaz maliyetinde bir faktördür. Açıklama için buraya bakın: Solidity Docs.
Son olarak, gerçekten tartışmak istediğim şey özel değiştiriciler. BoxingOracle.sol'ün 61. satırına bir göz atın:
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
"public" anahtar sözcüğünden hemen önceki onlyOwner
değiştiricisine dikkat edin. Bu, yalnızca sözleşmenin sahibinin bu yöntemi çağırabileceğini gösterir! Çok önemli olmakla birlikte, bu Solidity'nin yerel bir özelliği değildir (belki gelecekte olabilir). Aslında, onlyOwner
, kendimiz yarattığımız ve kullandığımız özel bir değiştirici örneğidir. Bir bakalım.
İlk olarak, değiştirici, BoxingOracle.sol'ün 3. satırında içe aktardığımızı görebileceğiniz Ownable.sol dosyasında tanımlanır:
import "./Ownable.sol"
Değiştiriciyi kullanmak için BoxingOracle
Ownable
. Ownable.sol'ün içinde, 25. satırda, "Ownable" sözleşmesinin içindeki değiştiricinin tanımını bulabiliriz:
modifier onlyOwner() { require(msg.sender == owner); _; }
(Bu arada, bu Sahip olunan sözleşme, OpenZeppelin'in kamu sözleşmelerinden birinden alınmıştır.)
Bu şeyin bir değiştirici olarak bildirildiğini ve bir işlevi değiştirmek için onu sahip olduğumuz gibi kullanabileceğimizi gösterdiğini unutmayın. Değiştiricinin etinin bir "zorunlu" ifadesi olduğuna dikkat edin. Require ifadeleri, bir tür iddia gibidir, ancak hata ayıklama için değildir. Require ifadesinin koşulu başarısız olursa, işlev bir istisna atar. Bu "gerektirme" ifadesini başka bir deyişle:
require(msg.sender == owner);
Bunun şu anlama geldiğini söyleyebiliriz:
if (msg.send != owner) throw an exception;
Ve aslında, Solidity 0.4.22 ve üzeri sürümlerde, bu require ifadesine bir hata mesajı ekleyebiliriz:
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
Son olarak, meraklı görünen satırda:
_;
Alt çizgi, "Burada, değiştirilmiş işlevin tüm içeriğini yürütün" ifadesinin kısaltmasıdır. Yani aslında, önce require ifadesi, ardından gerçek işlev yürütülür. Yani bu, bu mantık satırını değiştirilmiş fonksiyona önceden beklemek gibi.

Elbette değiştiricilerle yapabileceğiniz daha çok şey var. Dokümanları kontrol edin: Dokümanlar.
Sağlamlık Kitaplıkları
Solidity'nin kütüphane olarak bilinen bir dil özelliği vardır. DateLib.sol'deki projemizde bir örneğimiz var.
Bu, tarih türlerinin daha kolay işlenmesi için bir kitaplıktır. 4. satırda BoxingOracle'a aktarılır:
import "./DateLib.sol";
Ve 13. satırda kullanılır:
using DateLib for DateLib.DateTime;
DateLib.DateTime
, DateLib sözleşmesinden dışa aktarılan bir yapıdır (üye olarak sunulur; DateLib.sol'ün 4. satırına bakın) ve burada, belirli bir veri türü için DateLib kitaplığını "kullandığımızı" bildiriyoruz. Yani bu kütüphanede açıklanan yöntemler ve işlemler, olması gerektiğini söylediğimiz veri tipine uygulanacaktır. Solidity'de bir kütüphane bu şekilde kullanılır.
Daha açık bir örnek için, SafeMath gibi sayılar için OpenZeppelin'in kitaplıklarından bazılarına göz atın. Bunlar yerel (sayısal) Solidity veri türlerine uygulanabilir (oysa burada özel bir veri türüne bir kitaplık uyguladık) ve yaygın olarak kullanılır.
Arayüzler
Ana akım nesne yönelimli dillerde olduğu gibi, arayüzler desteklenir. Solidity'deki arabirimler sözleşmeler olarak tanımlanır, ancak işlevler için işlev gövdeleri atlanır. Bir arabirim tanımı örneği için bkz. OracleInterface.sol. Bu örnekte arayüz, içeriği ayrı bir adrese sahip ayrı bir sözleşmede bulunan Oracle sözleşmesi için bir vekil olarak kullanılır.
Adlandırma Kuralları
Elbette, adlandırma kuralları genel bir kural değildir; programcılar olarak, bize çekici gelen kodlama ve adlandırma kurallarını takip etmekte özgür olduğumuzu biliyoruz. Öte yandan, başkalarının kodumuzu okuma ve çalışma konusunda kendilerini rahat hissetmelerini istiyoruz, bu nedenle bir dereceye kadar standardizasyon arzu edilir.
Projeye Genel Bakış
Şimdi, söz konusu kod dosyalarında bulunan bazı genel dil özelliklerini gözden geçirdiğimize göre, bu proje için kodun kendisine daha spesifik bir şekilde bakmaya başlayabiliriz.
O halde bu projenin amacını bir kez daha netleştirelim. Bu projenin amacı, bir kehanet kullanan bir akıllı sözleşmenin yarı gerçekçi (veya sözde gerçekçi) bir gösterimini ve örneğini sağlamaktır. Özünde, bu sadece başka bir ayrı sözleşmeye çağrıda bulunan bir sözleşmedir.
Örneğin iş durumu aşağıdaki gibi ifade edilebilir:
- Bir kullanıcı, boks maçlarında farklı boyutlarda bahisler yapmak, bahisler için para (eter) ödemek ve ne zaman ve ne zaman kazanırsa kazançlarını toplamak ister.
- Bir kullanıcı bu bahisleri akıllı bir sözleşme ile yapar. (Gerçek hayattaki bir kullanım durumunda, bu, web3 ön ucuna sahip tam bir DApp olacaktır; ancak biz yalnızca sözleşme tarafını inceliyoruz.)
- Ayrı bir akıllı sözleşme - oracle - üçüncü bir tarafça yürütülür. Görevi, mevcut durumları (beklemede, devam ediyor, bitmiş vb.) ve bitmişse kazanan ile boks maçlarının bir listesini tutmaktır.
- Ana sözleşme, kahinden bekleyen eşleşmelerin listesini alır ve bunları kullanıcılara “bahis edilebilir” eşleşmeler olarak sunar.
- Ana sözleşme, bir maçın başlangıcına kadar olan bahisleri kabul eder.
- Bir maça karar verildikten sonra, ana sözleşme kazançları ve kayıpları basit bir algoritmaya göre böler, kendisi için bir pay alır ve talep üzerine kazançları öder (kaybedenler sadece tüm bahislerini kaybederler).
Bahis kuralları:
- Tanımlanmış, minimum bir bahis vardır (wei'de tanımlanmıştır).
- Maksimum bahis yoktur; Kullanıcılar minimum miktarın üzerinde istedikleri miktarda bahis oynayabilirler.
- Kullanıcılar, maç "devam ediyor" olana kadar bahis oynayabilir.
Kazançları bölmek için algoritma:
- Alınan tüm bahisler bir "pot"a yerleştirilir.
- Ev için tencereden küçük bir yüzde alınır.
- Her kazanan, bahislerinin göreceli boyutuyla doğru orantılı olarak potun bir oranını alır.
- Kazançlar, maça karar verildikten sonra, ilk kullanıcı sonuçları talep eder etmez hesaplanır.
- Kazançlar, kullanıcının isteği üzerine verilir.
- Beraberlik durumunda kimse kazanmaz - herkes hissesini geri alır ve kasa kesinti yapmaz.
BoxingOracle: Oracle Sözleşmesi
Sağlanan Ana Fonksiyonlar
Oracle'ın iki arabirimi vardır, diyebilirsiniz: biri sözleşmenin "sahibine" ve yürütücüsüne sunulur, diğeri ise genel halka sunulur; yani, kehaneti tüketen sözleşmeler. Sağlayıcı, sözleşmeye veri beslemek için işlevsellik sunar, esasen dış dünyadan veri alır ve blok zincirine koyar. Halka, söz konusu verilere salt okunur erişim sunar. Sözleşmenin kendisinin, sahip olmayanların herhangi bir veriyi düzenlemesini kısıtladığını, ancak bu verilere salt okunur erişimin kısıtlama olmaksızın herkese açık olarak verildiğini unutmamak önemlidir.
Kullanıcılara:
- Tüm eşleşmeleri listele
- Bekleyen eşleşmeleri listele
- Belirli bir maçın ayrıntılarını alın
- Belirli bir maçın durumunu ve sonucunu alın
sahibine:
- Bir eşleşme girin
- Maçın durumunu değiştir
- Maç sonucunu ayarla
Kullanıcı hikayesi:
- 9 Mayıs'ta yeni bir boks maçı duyurulur ve onaylanır.
- Ben, sözleşmeyi yürüten kişi (belki de tanınmış bir spor ağıyım veya yeni bir satış kanalıyım), yaklaşan eşleşmeyi Oracle'ın blok zincirindeki verilerine "beklemede" durumuyla ekliyorum. Artık herkes veya herhangi bir sözleşme bu verileri istedikleri gibi sorgulayabilir ve kullanabilir.
- Maç başladığında, o maçın durumunu "devam ediyor" olarak ayarladım.
- Maç bittiğinde, maçın durumunu "tamamlandı" olarak ayarladım ve kazananı belirtmek için maç verilerini değiştirdim.
Oracle Kod İncelemesi
Bu inceleme tamamen BoxingOracle.sol'e dayanmaktadır; satır numaraları bu dosyaya başvurur.
10. ve 11. satırlarda, maçlar için depolama yerimizi ilan ediyoruz:
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
yalnızca eşleşme örneklerini depolamak için basit bir dizidir ve eşleme yalnızca benzersiz bir eşleşme kimliğini (bir bytes32 değeri) dizideki dizinine eşlemek için bir tesistir, böylece biri bize bir eşleşmenin ham kimliğini verirse, bulmak için bu eşlemeyi kullanın.
17. satırda maç yapımız tanımlanmış ve açıklanmıştır:
//defines a match along with its outcome struct Match { bytes32 id; //unique id string name; //human-friendly name (eg, Jones vs. Holloway) string participants; //a delimited string of participant names uint8 participantCount; //number of participants (always 2 for boxing matches!) uint date; //GMT timestamp of date of contest MatchOutcome outcome; //the outcome (if decided) int8 winner; //index of the participant who is the winner } //possible match outcomes enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }
Satır 61: addMatch
işlevi yalnızca sözleşme sahibi tarafından kullanım içindir; saklanan verilere yeni bir eşleşme eklenmesine izin verir.
Satır 80: declareOutcome
işlevi, sözleşme sahibinin bir maçı "karar verildi" olarak belirlemesine ve kazanan katılımcıyı belirlemesine olanak tanır.
Satır 102-166: Aşağıdaki işlevlerin tümü halk tarafından çağrılabilir. Bu, genel olarak halka açık olan salt okunur verilerdir:
-
getPendingMatches
işlevi, mevcut durumu "beklemede" olan tüm eşleşmelerin kimliklerinin bir listesini döndürür. -
getAllMatches
işlevi, tüm eşleşmelerin kimliklerinin bir listesini döndürür. -
getMatch
işlevi, kimlik tarafından belirtilen tek bir eşleşmenin tüm ayrıntılarını döndürür.
193-204 satırları, temel olarak test etme, hata ayıklama ve tanılama için olan işlevleri bildirir.
- Fonksiyon
testConnection
sadece sözleşmeyi arayabileceğimizi test eder. -
getAddress
işlevi bu sözleşmenin adresini döndürür. -
addTestData
işlevi, eşleşme listesine bir grup test eşleşmesi ekler.
Sonraki adımlara geçmeden önce kodu biraz araştırmaktan çekinmeyin. Oracle sözleşmesini hata ayıklama modunda (bu serinin 1. Kısmında açıklandığı gibi) yeniden çalıştırmanızı, farklı işlevleri çağırmanızı ve sonuçları incelemenizi öneririm.
Boks Bahisleri: Müşteri Sözleşmesi
Müşteri sözleşmesinin (bahis sözleşmesi) nelerden sorumlu olduğunu ve nelerden sorumlu olmadığını belirlemek önemlidir. Müşteri sözleşmesi, gerçek boks maçlarının listelerini tutmaktan veya sonuçlarını bildirmekten sorumlu değildir . Bu hizmet için kahine “güveniyoruz” (evet biliyorum, hassas bir kelime var—uh oh—bunu Bölüm 3'te tartışacağız. Bahislerin kabul edilmesinden müşteri sözleşmesi sorumludur. Kazançları bölüştüren ve maçın sonucuna göre kazananların hesaplarına aktaran algoritmadan sorumludur (kahin'den alındığı gibi).
Ayrıca, her şey çekme tabanlıdır ve hiçbir olay veya itme yoktur. Sözleşme, verileri kehanetten alır. Sözleşme, maçın sonucunu kehanetten (kullanıcı isteğine yanıt olarak) çeker ve sözleşme, kazançları hesaplar ve kullanıcı isteğine yanıt olarak bunları aktarır.
Sağlanan Ana Fonksiyonlar
- Bekleyen tüm eşleşmeleri listele
- Belirli bir maçın ayrıntılarını alın
- Belirli bir maçın durumunu ve sonucunu alın
- Bahis parası koy
- Kazanç isteme/alma
Müşteri Kodu İncelemesi
Bu inceleme tamamen BoxingBets.sol'e dayanmaktadır; satır numaraları bu dosyaya başvurur.
Sözleşmedeki ilk kod satırları olan 12 ve 13. satırlar, sözleşmemizin verilerini depolayacağımız bazı eşlemeleri tanımlar.
12. satır, kullanıcı adreslerini kimlik listelerine eşler. Bu, bir kullanıcıyı, kullanıcıya ait olan bahislerin kimlikleri listesine eşler. Böylece, herhangi bir kullanıcı adresi için, o kullanıcı tarafından yapılmış tüm bahislerin bir listesini hızlı bir şekilde alabiliriz.
mapping(address => bytes32[]) private userToBets;
13. satır, bir maçın benzersiz kimliğini bir bahis örnekleri listesine eşler. Bununla, herhangi bir maç için, o maç için yapılmış tüm bahislerin bir listesini alabiliriz.
mapping(bytes32 => Bet[]) private matchToBets;
17. ve 18. satırlar kahinimizle bağlantıyla ilgilidir. İlk olarak, boxingOracleAddr
değişkeninde, Oracle sözleşmesinin adresini saklarız (varsayılan olarak sıfıra ayarlanır). Kahinin adresini sabit kodlayabilirdik ama o zaman asla değiştiremezdik. (Kâhinin adresini değiştirememek iyi ya da kötü bir şey olabilir - bunu Bölüm 3'te tartışabiliriz). Sonraki satır, Oracle'ın arabiriminin (OracleInterface.sol'de tanımlanan) bir örneğini oluşturur ve bunu bir değişkende saklar.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
58. satıra geçerseniz, bu oracle adresinin değiştirilebildiği ve boxingOracle
örneğinin yeni bir adresle yeniden başlatıldığı setOracleAddress
işlevini görürsünüz.
21. satır, wei cinsinden minimum bahis miktarımızı tanımlar. Bu tabii ki aslında çok küçük bir miktar, sadece 0.000001 eter.
uint internal minimumBet = 1000000000000;
Sırasıyla 58 ve 66. satırlarda setOracleAddress
ve getOracleAddress
işlevlerine sahibiz. setOracleAddress
, onlyOwner
değiştiricisine sahiptir, çünkü yalnızca sözleşmenin sahibi, Oracle'ı başka bir Oracle için değiştirebilir (muhtemelen iyi bir fikir değildir , ancak Bölüm 3'te ayrıntılı olarak ele alacağız). Öte yandan getOracleAddress
işlevi, genel olarak çağrılabilir; herkes hangi oracle'ın kullanıldığını görebilir.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
72. ve 79. satırlarda sırasıyla getBettableMatches
ve getMatch
işlevlerine sahibiz. Bunların yalnızca çağrıları kahine yönlendirdiğini ve sonucu döndürdüğünü unutmayın.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
placeBet
işlevi çok önemlidir (satır 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
Bunun çarpıcı bir özelliği, payable
değiştiricidir; Genel dil özelliklerini tartışmakla o kadar meşguldük ki, işlev çağrılarıyla birlikte para gönderebilmenin merkezi olarak önemli özelliğine henüz değinmedik! Temelde budur - gönderilen diğer argümanlar ve verilerle birlikte bir miktar parayı kabul edebilen bir işlevdir.
Buna burada ihtiyacımız var çünkü burası, kullanıcının aynı anda hangi bahsi yapacağını, bu bahse ne kadar para yatırmayı planladığını ve aslında parayı gönderdiğini tanımladığı yerdir. payable
değiştiricisi bunu sağlar. Bahsi kabul etmeden önce, bahsin geçerliliğini sağlamak için bir dizi kontrol yaparız. 111. satırdaki ilk kontrol:
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Gönderilen para miktarı msg.value
içinde saklanır. Tüm kontrollerin başarılı olduğunu varsayarsak, 123. satırda, bu miktarı kehanetin mülkiyetine aktaracağız, bu miktarın sahipliğini kullanıcıdan alıp sözleşmenin mülkiyetine geçireceğiz:
address(this).transfer(msg.value);
Son olarak, 136. satırda, sözleşmenin geçerli bir kehanete bağlı olup olmadığını bilmemize yardımcı olacak bir test/hata ayıklama yardımcı fonksiyonumuz var:
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Toplama
Ve bu aslında bu örneğe kadar; sadece bahsi kabul ediyorum. Kazançları bölme ve ödeme işlevi ve diğer bazı mantıklar, örneği sadece sözleşmeli bir kehanet kullanımını göstermek olan amacımız için yeterince basit tutmak için kasıtlı olarak dışarıda bırakılmıştır. Daha eksiksiz ve karmaşık bir mantık, şu anda bu örneğin bir uzantısı olan ve halen geliştirme aşamasında olan başka bir projede mevcuttur.
Şimdi kod tabanını daha iyi anladık ve Solidity tarafından sunulan bazı dil özelliklerini tartışmak için bir araç ve başlangıç noktası olarak kullandık. Bu üç bölümlük dizinin temel amacı, bir kahine sahip bir sözleşmenin kullanımını göstermek ve tartışmaktır. Bu bölümün amacı, bu özel kodu biraz daha iyi anlamak ve onu Solidity ve akıllı sözleşme geliştirmenin bazı özelliklerini anlamak için bir başlangıç noktası olarak kullanmaktır. Üçüncü ve son bölümün amacı, Oracle kullanımının stratejisini ve felsefesini ve kavramsal olarak akıllı sözleşme modeline nasıl uyduğunu tartışmak olacaktır.
Diğer Opsiyonel Adımlar
Daha fazlasını öğrenmek isteyen okuyucuları bu kodu alıp onunla oynamaya teşvik ediyorum. Yeni özellikler uygulayın. Herhangi bir hatayı düzeltin. Uygulanmamış özellikleri (ödeme arayüzü gibi) uygulayın. İşlev çağrılarını test edin. Bunları değiştirin ve ne olduğunu görmek için tekrar test edin. Bir web3 ön uç ekleyin. Kibritleri kaldırmak veya sonuçlarını değiştirmek için bir tesis ekleyin (hata durumunda). İptal edilen maçlar ne olacak? İkinci bir kehanet uygulayın. Tabii ki, bir sözleşme istediği kadar kahin kullanmakta serbesttir, ancak bu ne gibi sorunlara yol açar? Onunla iyi eğlenceler; bu, öğrenmenin harika bir yolu ve bu şekilde yaptığınızda (ve bundan zevk aldığınızda), öğrendiklerinizin daha fazlasını koruyacağınızdan emin olabilirsiniz.
Denenecek şeylerin kapsamlı olmayan örnek bir listesi:
- Hem sözleşmeyi hem de oracle'ı yerel test ağında çalıştırın (Bölüm 1'de açıklandığı gibi yer mantarı içinde) ve çağrılabilir tüm işlevleri ve tüm test işlevlerini çağırın.
- Bir maçın tamamlanmasının ardından kazançları hesaplamak ve bunları ödemek için işlevsellik ekleyin.
- Beraberlik durumunda tüm bahisleri iade etme işlevi ekleyin.
- Maç başlamadan önce para iadesi talep etmek veya bir bahsi iptal etmek için bir özellik ekleyin.
- Maçların bazen iptal edilebileceği gerçeğine izin vermek için bir özellik ekleyin (bu durumda herkesin geri ödemeye ihtiyacı olacaktır).
- Bir kullanıcı bahis yaptığında mevcut olan oracle'ın, o maçın sonucunu belirlemek için kullanılacak oracle ile aynı olduğunu garanti etmek için bir özellik uygulayın.
- Kendisiyle ilişkili bazı farklı özelliklere sahip olan veya muhtemelen boks dışında bir spora hizmet eden başka bir (ikinci) kahin uygulayın (katılımcıların sayıldığını ve listenin farklı spor türlerine izin verdiğini unutmayın, bu nedenle aslında sadece boksla sınırlı değiliz) .
-
getMostRecentMatch
, en son eklenen eşleşmeyi veya ne zaman gerçekleşeceği açısından geçerli tarihe en yakın eşleşmeyi döndürecek şekilde uygulayın. - İstisna işlemeyi uygulayın.
Sözleşme ve kahin arasındaki ilişkinin mekaniğine aşina olduğunuzda, bu üç bölümlük dizinin 3. Kısmında, bu örnekte ortaya çıkan bazı stratejik, tasarım ve felsefi konuları tartışacağız.