Contratti Ethereum Oracle: funzionalità del codice di solidità

Pubblicato: 2022-03-11

Nel primo segmento di questa tre parti, abbiamo esaminato un piccolo tutorial che ci ha fornito una semplice coppia contratto con Oracle. Sono stati descritti i meccanismi ei processi di configurazione (con tartufo), compilazione del codice, distribuzione su una rete di test, esecuzione e debug; tuttavia, molti dei dettagli del codice sono stati ignorati in modo ondulato. Quindi ora, come promesso, esamineremo alcune di quelle caratteristiche del linguaggio che sono uniche per lo sviluppo di contratti intelligenti Solidity e uniche per questo particolare scenario di contratto-oracolo. Anche se non possiamo esaminare scrupolosamente ogni singolo dettaglio (lo lascerò a te nei tuoi ulteriori studi, se lo desideri), cercheremo di individuare le caratteristiche più sorprendenti, più interessanti e più importanti del codice.

Per facilitare ciò, ti consiglio di aprire la tua versione del progetto (se ne hai una) o di avere il codice a portata di mano come riferimento.

Il codice completo a questo punto può essere trovato qui: https://github.com/jrkosinski/oracle-example/tree/part2-step1

Ethereum e Solidità

Solidity non è l'unico linguaggio di sviluppo di smart contract disponibile, ma penso che sia abbastanza sicuro dire che è il più comune e il più popolare in generale, per gli smart contract di Ethereum. Certamente è quello che ha il supporto e le informazioni più popolari, al momento in cui scrivo.

Diagramma delle caratteristiche cruciali di Ethereum Solidity

La solidità è orientata agli oggetti e completa di Turing. Detto questo, ti renderai presto conto dei suoi limiti incorporati (e completamente intenzionali), che rendono la programmazione dei contratti intelligenti molto diversa dal normale hacking Let's-do-this-thing.

Versione Solidità

Ecco la prima riga di ogni poesia in codice Solidity:

 pragma solidity ^0.4.17;

I numeri di versione che vedrai differiranno, poiché Solidity, ancora nella sua giovinezza, sta cambiando e si evolve rapidamente. La versione 0.4.17 è la versione che ho usato nei miei esempi; l'ultima versione al momento di questa pubblicazione è 0.4.25.

L'ultima versione in questo momento che stai leggendo potrebbe essere qualcosa di completamente diverso. Molte belle caratteristiche sono in lavorazione (o almeno pianificate) per Solidity, di cui parleremo tra poco.

Ecco una panoramica delle diverse versioni di Solidity.

Suggerimento per professionisti: puoi anche specificare una gamma di versioni (anche se non lo vedo fatto troppo spesso), in questo modo:

 pragma solidity >=0.4.16 <0.6.0;

Funzionalità del linguaggio di programmazione Solidity

La solidità ha molte caratteristiche del linguaggio che sono familiari alla maggior parte dei programmatori moderni, nonché alcune che sono distinte e (almeno per me) insolite. Si dice che sia stato ispirato da C++, Python e JavaScript, che mi sono tutti ben familiari personalmente, eppure Solidity sembra abbastanza distinto da qualsiasi di questi linguaggi.

Contrarre

Il file .sol è l'unità di base del codice. In BoxingOracle.sol, nota la nona riga:

 contract BoxingOracle is Ownable {

Poiché la classe è l'unità di base della logica nei linguaggi orientati agli oggetti, il contratto è l'unità di base della logica in Solidity. Basti per ora semplificare dire che il contratto è la “classe” di Solidity (per i programmatori orientati agli oggetti, questo è un salto facile).

Eredità

I contratti di solidità supportano completamente l'ereditarietà e funziona come ti aspetteresti; i contrattisti privati ​​non vengono ereditati, mentre quelli protetti e pubblici lo sono. Il sovraccarico e il polimorfismo sono supportati come ti aspetteresti.

 contract BoxingOracle is Ownable {

Nell'istruzione precedente, la parola chiave "is" indica l'ereditarietà. BoxingOracle eredita da Ownable. L'ereditarietà multipla è supportata anche in Solidity. L'ereditarietà multipla è indicata da un elenco delimitato da virgole di nomi di classi, in questo modo:

 contract Child is ParentA, ParentB, ParentC { …

Anche se (secondo me) non è una buona idea complicarsi eccessivamente quando si struttura il proprio modello di eredità, ecco un articolo interessante su Solidity in relazione al cosiddetto problema del diamante.

Enum

Le enumerazioni sono supportate in Solidity:

 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 }

Come ci si aspetterebbe (non diverso dai linguaggi familiari), a ciascun valore enum viene assegnato un valore intero, che inizia con 0. Come affermato nei documenti di Solidity, i valori enum sono convertibili in tutti i tipi interi (ad esempio, uint, uint16, uint32, ecc.), ma non è consentita la conversione implicita. Ciò significa che devono essere lanciati in modo esplicito (per uint, ad esempio).

Solidity Docs: Enums Enums Tutorial

Strutture

Gli struct sono un altro modo, come gli enum, per creare un tipo di dati definito dall'utente. Gli struct sono familiari a tutti i programmatori di base C/C++ e ai vecchi come me. Un esempio di struct, dalla riga 17 di BoxingOracle.sol:

 //defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }

Nota per tutti i vecchi programmatori C: il "packing" di Struct in Solidity è una cosa, ma ci sono alcune regole e avvertenze. Non presumere necessariamente che funzioni come in C; controlla i documenti e sii consapevole della tua situazione, per accertare se fare le valigie ti aiuterà o meno in un determinato caso.

Imballaggio della struttura di solidità

Una volta creati, gli struct possono essere indirizzati nel codice come tipi di dati nativi. Ecco un esempio della sintassi per "istanziazione" del tipo struct creato sopra:

 Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);

Tipi di dati nella solidità

Questo ci porta all'argomento di base dei tipi di dati in Solidity. Quali tipi di dati supporta la solidità? La solidità è tipizzata staticamente e al momento della stesura di questo i tipi di dati devono essere dichiarati in modo esplicito e associati a variabili.

Tipi di dati in Ethereum Solidity

Tipi di dati di solidità

booleani

I tipi booleani sono supportati con il nome bool e i valori true o false

Tipi numerici

I tipi interi sono supportati, sia con segno che senza segno, da int8/uint8 a int256/uint256 (rispettivamente da interi a 8 bit a interi a 256 bit). Il tipo uint è un'abbreviazione di uint256 (e allo stesso modo int è l'abbreviazione di int256).

In particolare, i tipi a virgola mobile non sono supportati. Perché no? Bene, per prima cosa, quando si tratta di valori monetari, è risaputo che le variabili a virgola mobile sono una cattiva idea (in generale ovviamente), perché il valore può essere perso nel nulla. I valori dell'etere sono indicati in wei, che è 1/1.000.000.000.000.000.000 di un etere e deve essere una precisione sufficiente per tutti gli scopi; non puoi scomporre un etere in parti più piccole.

I valori in virgola fissa sono parzialmente supportati in questo momento. Secondo i documenti di Solidity: “I numeri in virgola fissa non sono ancora completamente supportati da Solidity. Possono essere dichiarati, ma non possono essere assegnati a o da”.

https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9

Nota: nella maggior parte dei casi, è meglio utilizzare semplicemente uint, poiché diminuendo la dimensione della variabile (fino a uint32, ad esempio), si possono effettivamente aumentare i costi del gas anziché diminuirli come ci si potrebbe aspettare. Come regola generale, usa uint a meno che tu non sia certo di avere una buona ragione per fare diversamente.

Tipi di stringhe

Il tipo di dati stringa in Solidity è un argomento divertente; potresti ricevere opinioni diverse a seconda della persona con cui parli. Esiste un tipo di dati stringa in Solidity, questo è un dato di fatto. La mia opinione, probabilmente condivisa dalla maggior parte, è che non offre molte funzionalità. Analisi delle stringhe, concatenazione, sostituzione, taglio, persino contando la lunghezza della stringa: nessuna di quelle cose che probabilmente ti aspetteresti da un tipo di stringa è presente, quindi sono sotto la tua responsabilità (se ne hai bisogno). Alcune persone usano bytes32 al posto di string; anche questo si può fare.

Articolo divertente sulle corde Solidity

La mia opinione: potrebbe essere un esercizio divertente scrivere il proprio tipo di stringa e pubblicarlo per un uso generale.

Tipo di Indirizzo

Unici forse per Solidity, abbiamo un tipo di dati di indirizzo , in particolare per il portafoglio Ethereum o gli indirizzi dei contratti. È un valore di 20 byte specifico per la memorizzazione di indirizzi di quella particolare dimensione. Inoltre, ha membri di tipo specifici per indirizzi di quel tipo.

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

Tipi di dati degli indirizzi

Tipi DateTime

Non esiste un tipo Data o DateTime nativo in Solidity, di per sé, come ad esempio in JavaScript. (Oh no, la solidità suona sempre peggio ad ogni paragrafo!?) Le date sono nativamente indicate come timestamp di tipo uint (uint256). Sono generalmente gestiti come timestamp in stile Unix, in secondi anziché in millisecondi, poiché il timestamp del blocco è un timestamp in stile Unix. Nei casi in cui ti trovi ad aver bisogno di date leggibili dall'uomo per vari motivi, sono disponibili librerie open source. Potresti notare che ne ho usato uno in BoxingOracle: DateLib.sol. OpenZeppelin ha anche utilità di data e molti altri tipi di librerie di utilità generali (a breve arriveremo alla funzionalità della libreria di Solidity).

Suggerimento per professionisti: OpenZeppelin è una buona fonte (ma ovviamente non l'unica buona fonte) sia per la conoscenza che per il codice generico pre-scritto che può aiutarti a costruire i tuoi contratti.

Mappature

Si noti che la riga 11 di BoxingOracle.sol definisce qualcosa chiamato mappatura :

 mapping(bytes32 => uint) matchIdToIndex;

Una mappatura in Solidity è un tipo di dati speciale per ricerche rapide; essenzialmente una tabella di ricerca o simile ad una hashtable, in cui i dati contenuti risiedono sulla blockchain stessa (quando la mappatura è definita, come è qui, come membro di una classe). Nel corso dell'esecuzione del contratto, possiamo aggiungere dati alla mappatura, in modo simile all'aggiunta di dati a una tabella hash, e successivamente cercare i valori che abbiamo aggiunto. Nota ancora che in questo caso, i dati che aggiungiamo vengono aggiunti alla blockchain stessa, quindi persisterà. Se lo aggiungiamo alla mappatura di oggi a New York, tra una settimana qualcuno a Istanbul potrà leggerlo.

Esempio di aggiunta alla mappatura, dalla riga 71 di BoxingOracle.sol:

 matchIdToIndex[id] = newIndex+1

Esempio di lettura dalla mappatura, dalla riga 51 di BoxingOracle.sol:

 uint index = matchIdToIndex[_matchId];

Gli elementi possono anche essere rimossi dalla mappatura. Non è utilizzato in questo progetto, ma sarebbe simile a questo:

 delete matchIdToIndex[_matchId];

Valori di ritorno

Come avrai notato, Solidity potrebbe avere una somiglianza un po' superficiale con Javascript, ma non eredita gran parte della scioltezza di tipi e definizioni di JavaScript. Un codice di contratto deve essere definito in modo piuttosto rigoroso e ristretto (e questo è probabilmente un bene, considerando il caso d'uso). Con questo in mente, considera la definizione della funzione dalla riga 40 di BoxingOracle.sol

 function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }

OK, quindi, per prima cosa facciamo solo una rapida panoramica di ciò che è contenuto qui. function lo contrassegna come una funzione. _getMatchIndex è il nome della funzione (il trattino basso è una convenzione che indica un membro privato, ne parleremo più avanti). Richiede un argomento, denominato _matchId (questa volta viene utilizzata la convenzione di sottolineatura per denotare argomenti di funzione) di tipo bytes32 . La parola chiave private in realtà rende il membro privato nell'ambito, view dice al compilatore che questa funzione non modifica alcun dato sulla blockchain e infine: ~~~ solidity restituisce (uint) ~~~

Questo dice che la funzione restituisce un uint (una funzione che restituisce void semplicemente non avrebbe alcuna clausola di returns qui). Perché uint è tra parentesi? Questo perché le funzioni Solidity possono e spesso restituiscono tuple .

Consideriamo ora la seguente definizione dalla riga 166:

 function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }

Dai un'occhiata alla clausola di reso su questo! Restituisce uno, due... sette cose diverse. OK, quindi, questa funzione restituisce queste cose come una tupla. Come mai? Nel corso dello sviluppo, ti ritroverai spesso a dover restituire uno struct (se fosse JavaScript, probabilmente vorresti restituire un oggetto JSON). Ebbene, al momento della stesura di questo articolo (anche se in futuro ciò potrebbe cambiare), Solidity non supporta la restituzione di struct dalle funzioni pubbliche. Quindi devi invece restituire tuple. Se sei un tipo Python, potresti già sentirti a tuo agio con le tuple. Tuttavia, molte lingue non li supportano, almeno non in questo modo.

Vedere la riga 159 per un esempio di restituzione di una tupla come valore di ritorno:

 return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);

E come accettiamo il valore di ritorno di qualcosa del genere? Possiamo fare così:

 var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

In alternativa, puoi dichiarare le variabili in modo esplicito in anticipo, con i loro tipi corretti:

 //declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

E ora abbiamo dichiarato 7 variabili per contenere i 7 valori di ritorno, che ora possiamo usare. Altrimenti, supponendo di volere solo uno o due dei valori, possiamo dire:

 //declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);

Vedi cosa abbiamo fatto lì? Abbiamo solo i due che ci interessavano. Dai un'occhiata a tutte quelle virgole. Dobbiamo contarli con attenzione!

Importazioni

Le righe 3 e 4 di BoxingOracle.sol sono importazioni:

 import "./Ownable.sol"; import "./DateLib.sol";

Come probabilmente ti aspetteresti, si tratta di importare definizioni da file di codice che esistono nella stessa cartella di progetto dei contratti di BoxingOracle.sol.

Modificatori

Si noti che le definizioni delle funzioni hanno un sacco di modificatori allegati. In primo luogo, c'è la visibilità: visibilità privata, pubblica, interna ed esterna della funzione.

Inoltre, vedrai le parole chiave pure e view . Questi indicano al compilatore che tipo di modifiche apporterà la funzione, se presenti. Questo è importante perché una cosa del genere è un fattore nel costo finale del gas per l'esecuzione della funzione. Vedi qui per la spiegazione: Solidity Docs.

Infine, ciò di cui voglio davvero discutere, sono i modificatori personalizzati. Dai un'occhiata alla riga 61 di BoxingOracle.sol:

 function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {

Nota il modificatore onlyOwner appena prima della parola chiave "public". Ciò indica che solo il proprietario del contratto può chiamare questo metodo! Sebbene sia molto importante, questa non è una caratteristica nativa di Solidity (anche se forse lo sarà in futuro). In realtà, onlyOwner è un esempio di modificatore personalizzato che creiamo noi stessi e utilizziamo. Diamo un'occhiata.

Innanzitutto, il modificatore è definito nel file Ownable.sol, che come puoi vedere abbiamo importato alla riga 3 di BoxingOracle.sol:

 import "./Ownable.sol"

Nota che, per utilizzare il modificatore, abbiamo fatto ereditare BoxingOracle da Ownable . All'interno di Ownable.sol, alla riga 25, possiamo trovare la definizione del modificatore all'interno del contratto “Ownable”:

 modifier onlyOwner() { require(msg.sender == owner); _; }

(Questo contratto Ownable, tra l'altro, è tratto da uno degli appalti pubblici di OpenZeppelin.)

Nota che questa cosa è dichiarata come un modificatore, indicando che possiamo usarla come abbiamo, per modificare una funzione. Nota che la carne del modificatore è un'istruzione "richiedi". Le istruzioni Require sono un po' come le asserzioni, ma non per il debug. Se la condizione dell'istruzione require non riesce, la funzione genererà un'eccezione. Quindi, per parafrasare questa affermazione "richiedi":

 require(msg.sender == owner);

Potremmo dire che significa:

 if (msg.send != owner) throw an exception;

E, infatti, in Solidity 0.4.22 e versioni successive, possiamo aggiungere un messaggio di errore all'istruzione require:

 require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");

Infine, nella linea curiosa:

 _;

Il carattere di sottolineatura è l'abbreviazione di "Qui, esegui l'intero contenuto della funzione modificata". Quindi, in effetti, verrà eseguita prima l'istruzione require, seguita dalla funzione effettiva. Quindi è come anteporre questa linea logica alla funzione modificata.

Ci sono, ovviamente, più cose che puoi fare con i modificatori. Controlla i documenti: Documenti.

Biblioteche della Solidità

Esiste una caratteristica del linguaggio di Solidity nota come libreria . Abbiamo un esempio nel nostro progetto su DateLib.sol.

Implementazione della libreria di solidità!

Questa è una libreria per una migliore gestione più semplice dei tipi di data. È importato in BoxingOracle alla riga 4:

 import "./DateLib.sol";

Ed è usato alla riga 13:

 using DateLib for DateLib.DateTime;

DateLib.DateTime è uno struct che viene esportato dal contratto DateLib (è esposto come membro; vedere la riga 4 di DateLib.sol) e stiamo dichiarando qui che stiamo "usando" la libreria DateLib per un determinato tipo di dati. Quindi i metodi e le operazioni dichiarati in quella libreria si applicheranno al tipo di dati che abbiamo detto dovrebbe. Ecco come viene utilizzata una libreria in Solidity.

Per un esempio più chiaro, controlla alcune delle librerie di OpenZeppelin per i numeri, come SafeMath. Questi possono essere applicati a tipi di dati Solidity nativi (numerici) (mentre qui abbiamo applicato una libreria a un tipo di dati personalizzato) e sono ampiamente utilizzati.

Interfacce

Come nei principali linguaggi orientati agli oggetti, le interfacce sono supportate. Le interfacce in Solidity sono definite come contratti, ma i corpi delle funzioni sono omessi per le funzioni. Per un esempio di definizione di interfaccia, vedere OracleInterface.sol. In questo esempio, l'interfaccia viene utilizzata come sostituto del contratto Oracle, il cui contenuto risiede in un contratto separato con un indirizzo separato.

Convenzioni di denominazione

Naturalmente, le convenzioni di denominazione non sono una regola globale; come programmatori, sappiamo che siamo liberi di seguire le convenzioni di codifica e denominazione che ci interessano. D'altra parte, vogliamo che gli altri si sentano a proprio agio leggendo e lavorando con il nostro codice, quindi è auspicabile un certo grado di standardizzazione.

Panoramica del progetto

Quindi, ora che abbiamo esaminato alcune caratteristiche generali del linguaggio presenti nei file di codice in questione, possiamo iniziare a dare un'occhiata più specifica al codice stesso, per questo progetto.

Quindi, chiariamo ancora una volta lo scopo di questo progetto. Lo scopo di questo progetto è fornire una dimostrazione semi-realistica (o pseudo-realistica) e un esempio di contratto intelligente che utilizza un oracolo. In fondo, questo è solo un contratto che richiama un altro contratto separato.

Il business case dell'esempio può essere espresso come segue:

  • Un utente vuole fare scommesse di dimensioni diverse sugli incontri di boxe, pagando denaro (etere) per le scommesse e raccogliendo le loro vincite quando e se vincono.
  • Un utente effettua queste scommesse tramite un contratto intelligente. (In un caso d'uso reale, questa sarebbe una DApp completa con un front-end web3; ma stiamo solo esaminando il lato dei contratti.)
  • Un contratto intelligente separato, l'oracolo, è gestito da una terza parte. Il suo compito è mantenere un elenco degli incontri di boxe con i loro stati attuali (in attesa, in corso, terminati, ecc.) e, se terminati, il vincitore.
  • Il contratto principale riceve elenchi di partite in sospeso dall'oracolo e le presenta agli utenti come partite "scommesse".
  • Il contratto principale accetta scommesse fino all'inizio di una partita.
  • Dopo che una partita è stata decisa, il contratto principale divide le vincite e le sconfitte secondo un semplice algoritmo, prende un taglio per sé e paga le vincite su richiesta (i perdenti perdono semplicemente l'intera puntata).

Le regole delle scommesse:

  • C'è una puntata minima definita (definita in wei).
  • Non esiste una puntata massima; gli utenti possono scommettere qualsiasi importo che vogliono al di sopra del minimo.
  • Gli utenti possono piazzare scommesse fino al momento in cui la partita diventa "in corso".

Algoritmo per dividere la vincita:

  • Tutte le scommesse ricevute vengono piazzate in un "piatto".
  • Una piccola percentuale viene rimossa dal piatto, per la casa.
  • Ogni vincitore riceve una parte del piatto, direttamente proporzionale alla dimensione relativa delle sue scommesse.
  • Le vincite vengono calcolate non appena il primo utente richiede i risultati, dopo che la partita è stata decisa.
  • Le vincite vengono assegnate su richiesta dell'utente.
  • In caso di pareggio, nessuno vince: tutti si riprendono la puntata e la casa non riceve alcun taglio.

BoxingOracle: il contratto Oracle

Principali funzioni fornite

L'oracolo ha due interfacce, si potrebbe dire: una presentata al “proprietario” e manutentore del contratto e una presentata al grande pubblico; cioè contratti che consumano l'oracolo. Il manutentore, offre funzionalità per inserire i dati nel contratto, essenzialmente prendendo i dati dal mondo esterno e inserendoli nella blockchain. Al pubblico, offre accesso in sola lettura a tali dati. È importante notare che il contratto stesso impedisce ai non proprietari di modificare i dati, ma l'accesso in sola lettura a tali dati è concesso pubblicamente senza restrizioni.

Agli utenti:

  • Elenca tutte le corrispondenze
  • Elenca le partite in sospeso
  • Ottieni i dettagli di una partita specifica
  • Ottieni lo stato e l'esito di una partita specifica

Al proprietario:

  • Inserisci una corrispondenza
  • Cambia lo stato della partita
  • Imposta l'esito della partita

Illustrazione degli elementi di accesso dell'utente e del proprietario

Storia dell'utente:

  • Viene annunciato e confermato un nuovo incontro di boxe per il 9 maggio.
  • Io, mantenitore del contratto (forse sono una nota rete sportiva o un nuovo outlet), aggiungo la partita imminente ai dati dell'oracolo sulla blockchain, con lo stato "in sospeso". Chiunque o qualsiasi contratto ora può interrogare e utilizzare questi dati come preferisce.
  • Quando la partita inizia, ho impostato lo stato di quella partita su "in corso".
  • Al termine della partita, imposto lo stato della partita su "completato" e modifico i dati della partita per indicare il vincitore.

Revisione del codice Oracle

Questa recensione è basata interamente su BoxingOracle.sol; i numeri di riga fanno riferimento a quel file.

Alle righe 10 e 11 dichiariamo il nostro deposito per i fiammiferi:

 Match[] matches; mapping(bytes32 => uint) matchIdToIndex;

matches è solo un semplice array per la memorizzazione di istanze di corrispondenza e la mappatura è solo una struttura per mappare un ID di corrispondenza univoco (un valore bytes32) al suo indice nell'array in modo che se qualcuno ci consegna un ID grezzo di una corrispondenza, possiamo usa questa mappatura per individuarlo.

Alla riga 17, la nostra struttura di corrispondenza è definita e spiegata:

 //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 }

Riga 61: La funzione addMatch è ad uso esclusivo del titolare del contratto; consente l'aggiunta di una nuova corrispondenza ai dati memorizzati.

Riga 80: La funzione dichiarare l' declareOutcome consente al titolare del contratto di impostare una partita come "decisa", impostando il partecipante che ha vinto.

Righe 102-166: Le seguenti funzioni sono tutte richiamabili dal pubblico. Questi sono i dati di sola lettura aperti al pubblico in generale:

  • La funzione getPendingMatches restituisce un elenco di ID di tutte le corrispondenze il cui stato corrente è "in sospeso".
  • La funzione getAllMatches restituisce un elenco di ID di tutte le corrispondenze.
  • La funzione getMatch restituisce i dettagli completi di una singola corrispondenza, specificata dall'ID.

Le righe 193-204 dichiarano funzioni che servono principalmente a test, debugging e diagnostica.

  • Function testConnection verifica solo che siamo in grado di chiamare il contratto.
  • La funzione getAddress restituisce l'indirizzo di questo contratto.
  • La funzione addTestData aggiunge un gruppo di corrispondenze di prova all'elenco delle corrispondenze.

Sentiti libero di esplorare un po' il codice prima di passare ai passaggi successivi. Suggerisco di eseguire nuovamente il contratto Oracle in modalità debug (come descritto nella Parte 1 di questa serie), chiamare diverse funzioni ed esaminare i risultati.

BoxingBets: il contratto del cliente

È importante definire di cosa è responsabile il contratto del cliente (il contratto di scommesse) e di cosa non è responsabile. Il contratto con il cliente non è responsabile della conservazione degli elenchi degli incontri di boxe reali o della dichiarazione dei loro risultati. Noi "fidiamo" (sì lo so, c'è quella parola delicata - uh oh - ne parleremo nella Parte 3) dell'oracolo per quel servizio. Il contratto del cliente è responsabile dell'accettazione delle scommesse. È responsabile dell'algoritmo che divide le vincite e le trasferisce sui conti dei vincitori in base all'esito della partita (come ricevuto dall'oracolo).

Inoltre, tutto è basato su pull e non ci sono eventi o push. Il contratto estrae i dati dall'oracolo. Il contratto estrae l'esito della partita dall'oracolo (in risposta alla richiesta dell'utente) e il contratto calcola le vincite e le trasferisce in risposta alla richiesta dell'utente.

Principali funzioni fornite

  • Elenca tutte le partite in sospeso
  • Ottieni i dettagli di una partita specifica
  • Ottieni lo stato e l'esito di una partita specifica
  • Piazza una scommessa
  • Richiedi/ricevi vincite

Revisione del codice cliente

Questa recensione si basa interamente su BoxingBets.sol; i numeri di riga fanno riferimento a quel file.

Le righe 12 e 13, le prime righe di codice del contratto, definiscono alcune mappature in cui memorizzeremo i dati del nostro contratto.

La riga 12 associa gli indirizzi degli utenti a elenchi di ID. Questo sta mappando un utente su un elenco di ID di scommesse che appartengono all'utente. Quindi, per ogni dato indirizzo utente, possiamo ottenere rapidamente un elenco di tutte le scommesse che sono state fatte da quell'utente.

 mapping(address => bytes32[]) private userToBets;

La riga 13 associa l'ID univoco di una partita a un elenco di istanze di scommessa. Con questo, possiamo, per una data partita, ottenere un elenco di tutte le scommesse che sono state fatte per quella partita.

 mapping(bytes32 => Bet[]) private matchToBets;

Le righe 17 e 18 sono relative al collegamento con il nostro oracolo. Innanzitutto, nella variabile boxingOracleAddr , memorizziamo l'indirizzo del contratto Oracle (impostato a zero per impostazione predefinita). Potremmo codificare l'indirizzo dell'oracolo, ma non saremo mai in grado di cambiarlo. (Non essere in grado di cambiare l'indirizzo dell'oracolo potrebbe essere una cosa buona o cattiva, possiamo discuterne nella Parte 3). La riga successiva crea un'istanza dell'interfaccia di Oracle (che è definita in OracleInterface.sol) e la memorizza in una variabile.

 //boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);

Se vai avanti alla riga 58, vedrai la funzione setOracleAddress , in cui questo indirizzo di Oracle può essere modificato e in cui l'istanza di boxingOracle viene creata nuovamente con un nuovo indirizzo.

La riga 21 definisce la nostra puntata minima, in wei. Questa è ovviamente una quantità molto piccola, solo 0,000001 etere.

 uint internal minimumBet = 1000000000000;

Alle righe 58 e 66 rispettivamente abbiamo le setOracleAddress e getOracleAddress . Il setOracleAddress ha il modificatore onlyOwner perché solo il proprietario del contratto può sostituire l'oracolo con un altro oracolo (probabilmente non è una buona idea, ma lo approfondiremo nella Parte 3). La funzione getOracleAddress , d'altra parte, è pubblicamente richiamabile; chiunque può vedere quale oracolo viene utilizzato.

 function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....

Alle righe 72 e 79 abbiamo rispettivamente le funzioni getBettableMatches e getMatch . Nota che questi stanno semplicemente inoltrando le chiamate all'oracolo e restituendo il risultato.

 function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....

La funzione placeBet è molto importante (riga 108).

 function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...

Una caratteristica sorprendente di questo è il modificatore payable ; siamo stati così impegnati a discutere le caratteristiche generali del linguaggio che non abbiamo ancora toccato l'importante caratteristica di poter inviare denaro insieme alle chiamate di funzione! Questo è fondamentalmente quello che è: è una funzione che può accettare una somma di denaro insieme a qualsiasi altro argomento e dato inviato.

Ne abbiamo bisogno qui perché è qui che l'utente definisce simultaneamente quale scommessa farà, quanti soldi intende avere per quella scommessa e invia effettivamente i soldi. Il modificatore payable lo consente. Prima di accettare la scommessa, facciamo una serie di controlli per garantire la validità della scommessa. Il primo controllo alla linea 111 è:

 require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");

La quantità di denaro inviata viene archiviata in msg.value . Supponendo che tutti i controlli siano superati, alla riga 123, trasferiremo tale importo in proprietà dell'oracolo, sottraendo all'utente la proprietà di tale importo e in possesso del contratto:

 address(this).transfer(msg.value);

Infine, alla riga 136, abbiamo una funzione di test/debugging che ci aiuterà a sapere se il contratto è collegato o meno a un oracolo valido:

 function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }

Avvolgendo

E questo è in realtà per quanto riguarda questo esempio; solo accettando la scommessa. La funzionalità per dividere le vincite e pagare, così come qualche altra logica è stata volutamente tralasciata per mantenere l'esempio abbastanza semplice per il nostro scopo, che è semplicemente quello di dimostrare l'uso di un oracolo con un contratto. Quella logica più completa e complessa esiste attualmente in un altro progetto, che è un'estensione di questo esempio ed è ancora in fase di sviluppo.

Quindi ora abbiamo una migliore comprensione della base di codice e l'abbiamo usata come veicolo e punto di partenza per discutere alcune delle funzionalità del linguaggio offerte da Solidity. Lo scopo principale di questa serie in tre parti è dimostrare e discutere l'uso di un contratto con un oracolo. Lo scopo di questa parte è capire un po' meglio questo codice specifico e usarlo come punto di partenza per comprendere alcune caratteristiche di Solidity e dello sviluppo di smart contract. Lo scopo della terza e ultima parte sarà quello di discutere la strategia e la filosofia dell'utilizzo di Oracle e come si inserisce concettualmente nel modello di smart contract.

Ulteriori passaggi opzionali

Incoraggio vivamente i lettori che desiderano saperne di più, a prendere questo codice e giocarci. Implementa nuove funzionalità. Correggi eventuali bug. Implementa funzionalità non implementate (come l'interfaccia di pagamento). Testare le chiamate di funzione. Modificali e riprova per vedere cosa succede. Aggiungi un front-end web3. Aggiungi una struttura per rimuovere le partite o modificarne i risultati (in caso di errore). E le partite annullate? Implementa un secondo oracolo. Naturalmente, un contratto è libero di utilizzare tutti gli oracoli che vuole, ma quali problemi comporta? Divertiti con esso; è un ottimo modo per imparare, e quando lo fai in quel modo (e ne trai divertimento) sei sicuro di conservare più di ciò che hai imparato.

Un esempio di elenco non completo di cose da provare:

  • Esegui sia il contratto che l'oracolo in testnet locale (in tartufo, come descritto nella Parte 1) e chiama tutte le funzioni richiamabili e tutte le funzioni di test.
  • Aggiungi funzionalità per calcolare le vincite e pagarle, al termine di una partita.
  • Aggiungi funzionalità per rimborsare tutte le scommesse in caso di pareggio.
  • Aggiungi una funzione per richiedere il rimborso o l'annullamento di una scommessa, prima dell'inizio della partita.
  • Aggiungi una funzione per tenere conto del fatto che le partite a volte possono essere annullate (nel qual caso tutti avranno bisogno di un rimborso).
  • Implementa una funzione per garantire che l'oracolo che era presente quando un utente ha piazzato una scommessa sia lo stesso oracolo che verrà utilizzato per determinare il risultato di quella partita.
  • Implementa un altro (secondo) oracolo, che ha alcune caratteristiche diverse ad esso associate, o forse serve uno sport diverso dalla boxe (nota che il conteggio dei partecipanti e l'elenco consentono diversi tipi di sport, quindi in realtà non siamo limitati alla sola boxe) .
  • Implementa getMostRecentMatch in modo che restituisca effettivamente la corrispondenza aggiunta più di recente o la corrispondenza più vicina alla data corrente in termini di quando si verificherà.
  • Implementare la gestione delle eccezioni.

Dopo aver acquisito familiarità con i meccanismi della relazione tra il contratto e l'oracolo, nella parte 3 di questa serie in tre parti, discuteremo alcune delle questioni strategiche, di design e filosofiche sollevate da questo esempio.