Blockchain for Developers
Software Developers Arena
16 February 2018
Sergio Perticone
Skillbill srl
Sergio Perticone
Skillbill srl
È un database distribuito di transazioni
Un po' come BitTorrent, ma più delicato:
Spauracchio del sistema: double spending
Ottobre 2008:
Bitcoin: A Peer-to-Peer Electronic Cash System
Fondamenta teoriche della blockchain
Rilascio del codice e live da gennaio 2009
Ad alto livello:
type Block struct { // Metadata Timestamp time.Time Index int // Payload tx []Transaction // Previous block's hash PrevHash []byte // Proof of work Difficulty int Nonce int }
PrevHash
è ciò che forma la chain
I miner sono i peer che producono i blocchi
Qualunque peer può essere il miner di un blocco
Incentivi:
Qual è il trucco?
Limitare artificialmente la capacità di creare blocchi
Un nodo accetta un blocco solo se il miner riesce a provare di aver risolto un problema
Il lavoro del miner viene convertito in token (ricompensa)
In caso di ambiguità un nodo sceglie la chain più lunga (+ lavoro)
Grazie alla PoW:
Bitcoin ha mutuato la PoW da Hashcash [Dwork, Naor 1992]
Idea: il francobollo per la posta elettronica
Ricorda qualcosa?
Esempio di francobollo:
X-Hashcash: 1:24:180213122530:sergio@localhost::82gsa6jcvgsKQQ:fXAG
Campi:
type Header struct { version int // always 1 difficulty int date Timestamp // YYMMDDhhmmss resource string // recipient extension string // unused random string // base64 encoded nonce Nonce // base64 encoded }
Per essere valido l'header deve avere questa proprietà:
$ echo -n 1:24:180213122530:sergio@localhost::82gsa6jcvgsKQQ:fXAG | sha1sum 0000007a89c9a369092a1248b830aa5bd8db3d38 -
Il suo hash SHA-1
deve essere "piccolo"
lavoro
=
#bit più significativi uguali a 0
Non è triviale, computazionalmente, perché SHA-1
è una funzione di hash crittografica
package main
import (
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"flag"
"fmt"
"io"
"time"
)
type Header struct {
version int // always 1
difficulty int
date Timestamp // YYMMDDhhmmss
resource string // recipient
extension string // unused
random string // base64 encoded
nonce Nonce // base64 encoded
}
func (h *Header) String() string {
return fmt.Sprintf("%v:%v:%v:%v::%v:%v",
h.version,
h.difficulty, h.date,
h.resource, h.random, h.nonce)
}
func (h *Header) Hash() [sha1.Size]byte {
str := fmt.Sprintf("%v", h)
return sha1.Sum([]byte(str))
}
func (h *Header) HashIsValid() bool {
hash := h.Hash()
// check first nbytes against 0
nbytes := h.difficulty / 8
for _, b := range hash[:nbytes] {
if b != 0x00 {
return false
}
}
// check remaining bits
nbits := h.difficulty - 8*nbytes
if nbits > 0 {
mask := uint(0xff) >> uint(nbits)
b := uint(hash[nbytes])
if (b | mask) != mask {
return false
}
}
return true
}
type Timestamp time.Time
func (ts Timestamp) String() string {
t := time.Time(ts)
yr, mon, day := t.Date()
return fmt.Sprintf("%02d%02d%02d%02d%02d%02d",
yr-2000, mon, day, t.Hour(), t.Minute(), t.Second())
}
type Nonce uint32
const (
NonceByteSize = 3
NonceMax = 1<<20 - 1
)
func NewNonce() Nonce {
buf := random(4)
v := binary.LittleEndian.Uint32(buf) & NonceMax
return Nonce(v)
}
func (c *Nonce) Next() error {
*c++
if *c > NonceMax {
return fmt.Errorf("overflow")
}
return nil
}
func (c Nonce) String() string {
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, uint32(c))
return fmt.Sprintf("%v", b64(buf[:NonceByteSize]))
}
func Hashcash(difficulty int, resource string, output chan<- *Header) { h := &Header{ version: 1, difficulty: difficulty, date: Timestamp(time.Now()), resource: resource, } setup: h.random = b64(random(10)) h.nonce = NewNonce() for { if h.HashIsValid() { output <- h return } err := h.nonce.Next() if err != nil { // overflow goto setup } } }
var cfg struct {
rsc string
difficulty int
njobs int
verbose bool
help bool
}
func init() {
flag.StringVar(&cfg.rsc, "r", "sergio@localhost", "resource")
flag.IntVar(&cfg.difficulty, "d", 20, "difficulty (bits)")
flag.IntVar(&cfg.njobs, "j", 1, "number of concurrent jobs")
flag.BoolVar(&cfg.help, "h", false, "help")
}
func main() {
flag.Parse()
if cfg.help || cfg.difficulty < 0 || cfg.njobs <= 0 {
flag.PrintDefaults()
return
}
ch := make(chan *Header)
for i := 0; i < cfg.njobs; i++ {
go Hashcash(cfg.difficulty, cfg.rsc, ch)
}
clock := time.Tick(100 * time.Millisecond)
for {
select {
case header := <-ch:
fmt.Println()
fmt.Println(header)
return
case <-clock:
fmt.Printf(".")
}
}
}
func random(size int64) []byte {
buf := make([]byte, size)
r := io.LimitReader(rand.Reader, size)
r.Read(buf)
return buf
}
func b64(buf []byte) string {
enc := base64.StdEncoding.WithPadding(base64.NoPadding)
return enc.EncodeToString(buf)
}
func (m *miner) mine(output chan<- *Block) { block := m.prepareNextBlock() block.Nonce = rand.Int() for i := 0; i < maxRounds; i++ { if m.chain.ProofIsValid(block.Difficulty, block.Hash()) { output <- block return } block.Nonce++ } output <- nil }
func (c *Chain) ProofIsValid(difficulty int, hash []byte) bool { // check first nbytes against 0 nbytes := difficulty / 8 for _, b := range hash[:nbytes] { if b != 0x00 { return false } } // check remaining bits nbits := uint(difficulty - 8*nbytes) if nbits == 0 { return true } if nbits > 0 { mask := uint(0xff >> nbits) b := uint(hash[nbytes]) if (b | mask) != mask { return false } } return true }
Alla Proof of Work (Waste?) c'è una possibile alternativa
funzione randomica che assegna il diritto di creare un blocco
La funzione è pesata proporzionalmente a quanto un candidato (validator) scommette di essere lui il prescelto
Le transazioni hanno questa struttura:
type Transaction struct { Inputs []UTXO Outputs []string // addresses Tokens []int // (not using a map just to look like bitcoin) PubKey crypto.PublicKey Signature crypto.Signature }
I'oggetto di una transazione sono gli UTXO
type UTXO struct { OutputIndex int TxIndex int BlockIndex int }
type UTXOSet struct { sync.RWMutex data map[string][]UTXO }
func (set UTXOSet) Get(address string) []UTXO func (set UTXOSet) Add(address string, utxo UTXO) func (set UTXOSet) Remove(address string, utxo UTXO) func (set UTXOSet) Contains(address string, utxo UTXO) bool
Saldo di un conto sulla Toychain:
func (c *Chain) fundsFromUTXO(utxo UTXO) int { // ... b := c.Block(utxo.BlockIndex) tx := b.tx[utxo.TxIndex] return tx.Tokens[utxo.OutputIndex] }
func (c *Chain) Funds(address string) int { funds := 0 for _, utxo := range c.utxoset.Get(address) { funds += c.fundsFromUTXO(utxo) } return funds }
Trasferimento di UTXO
for i, tx := range block.tx { // destroy inputs: for _, utxo := range tx.Inputs { addr := tx.PubKey.Address() c.utxoset.Remove(addr, utxo) } // distribute outputs: for j, output := range tx.Outputs { c.utxoset.Add(output, UTXO{ BlockIndex: block.Index, TxIndex: i, OutputIndex: j, }) } // ...
La sicurezza di Bitcoin è basata sulla crittografia a chiave asimmetrica
L'indirizzo è derivato dalla chiave pubblica
func (k *publicKey) Address() string { str := fmt.Sprint(*k.data) hash := Hash([]byte(str)) return encoder.EncodeToString(hash) }
Esempi di indirizzi:
LnvGRRelRQFiUw0QaIbfzKO0yW4jebBcIpEcqjIejCI
14v6afb9yYFmM3fozN54yxsEtBCLVmoRwx
Alice vuole pagare token a Bob:
I token trasferiti saranno spendibili solo da chi possiede la chiave privata di Bob
func (p *payer) PayToAddress(address string, amount int) error { myaddr := p.privKey.Public().Address() myutxo := p.chain.utxoset.Get(myaddr) if len(myutxo) == 0 { return fmt.Errorf("no utxo for %v", myaddr) } mytokens := make([]int, len(myutxo)) for i, utxo := range myutxo { mytokens[i] = p.chain.fundsFromUTXO(utxo) } inputs, change, err := p.genInputs(myutxo, mytokens, amount) if err != nil { return err } outputs, tokens := []string{address}, []int{amount} if change > 0 { // give me back the change, no fees in toychain outputs = append(outputs, myaddr) tokens = append(tokens, change) } tx := NewTx(inputs, outputs, tokens) tx.Sign(p.privKey) p.chain.SubmitTransaction(tx) return nil }
func (c *Chain) checkTransaction(tx Transaction) error { var funds, output int valid := tx.PubKey.Verify(tx.Hash(), tx.Signature) if !valid { return fmt.Errorf("invalid signature") } addr := tx.PubKey.Address() for _, utxo := range tx.Inputs { if c.utxoset.Contains(addr, utxo) == false { return fmt.Errorf("invalid inputs") } funds += c.fundsFromUTXO(utxo) } for i := range tx.Tokens { output += tx.Tokens[i] } if funds < output { return fmt.Errorf("not enough funds: %v < %v", funds, output) } return nil }
Per evitare il double spending, sono previste le seguenti limitazioni:
Il termine Smart Contract viene introdotto da Nick Szabo nel 1996
Szabo descrive la possibilità di creare contratti tra estranei con la mediazione di protocolli software e l'aiuto di tecniche crittografiche
Questi contratti devono possedere:
Possiamo usare la blockchain?
In effetti abbiamo semplificato un po', la verifica delle transazioni è un po' più sofisticata
Ad una transazione è associato un lucchetto digitale sotto forma di programma
Ogni nodo esegue una VM
Un transazione viene effettuata tramite l'esecuzione di uno script
Sketch di funzionamento della VM:
for { input := nextInput() if input.isEOF() { return stack.pop() } if input.isData() { stack.push(input.Data()) continue } switch input.OPCode() { case OP_DUP: v := stack.pop() stack.push(v) stack.push(v) ... } }
Gli script per Bitcoin sono un'espressione RPN della forma:
[ DATO... ] [ DATO | OPCODE ]... input programma
Scenario: Alice vuole pagare Bob
OP_DUP OP_HASH160 $hash_pubkey_bob OP_EQUAL OP_VERIFY OP_CHECKSIG
Scenario: Bob vuole spendere i bitcoin ricevuti di Alice
$signature_bob $pubkey_bob OP_DUP OP_HASH160 $hash_pubkey_bob OP_EQUAL OP_VERIFY OP_CHECKSIG
Se il risultato è true la transazione può essere confermata
$m $pubkey_1 ... $pubkey_n $n OP_CHECKMULTISIG
OP_SHA256 OP_SHA256 $given_hash EQUAL
$expire_value OP_CHECKLOCKTIMEVERIFY OP_DROP
OP_TRUE
OP_RETURN
Gli Smart Contract su bitcoin non sembrano poi così "smart"
A Next-Generation Smart Contract and Decentralized Application Platform
Idea: Sfruttare la tecnologia della blockchain e generalizzarla
Non piattaforma che esegue contratti per potere scambiare valuta digitale
Ma una macchina distribuita per contratti arbitrari
Una sorta di blockchain universale (ERC20)
Non ci sono UTXO
Ad un account Ethereum è invece associato uno stato:
Un transazione corrisponde ad un cambio di stato
Esistono due tipi di account
Esterni
Contratti
160 bit meno significativi dell'hash della chiave pubblica
indirizzo = SHA-3(pubKey) & (1<<160) - 1
Esempio: 0x627306090abaB3A6e1400e9345bC60c78a8BEf57
La EVM va a benzina (gas), in questo modo è possibile avere un upper bound per un dato programma
Un cambio di stato consuma gas
Per ogni tx bisogna specificare:
Se la benzina è sufficiente per la tx, l'eventuale avanzo viene rimborsato
Altrimenti la transazione è annullata (lo stato non cambia)
I costi di commissione sono il prodotto tra il gas consumato e il prezzo proposto
Ethereum ha una moneta nativa: ether
Protocollo → Ethereum
Moneta → ether (ETH)
La blockchain di Ethereum non esiste per supportare la moneta, ma il contrario
Gli ether sono il meccanismo per regolare le risorse
wei
(10e-18 ETH)Il linguaggio "contract-oriented" per lo sviluppo di Smart Contract su Ethereum
Does anybody remember DAO?
Alcuni problemi di design:
In generale sono problemi ben affrontati in letteratura e molti risolti in linguaggi general purpose
contract Voting { mapping (bytes32 => uint8) public votes; mapping (bytes32 => bool) public validCandidates; function Voting(bytes32[] candidates) public { for (uint i=0; i<candidates.length; i++) { validCandidates[candidates[i]] = true; } } function totalVotesFor(bytes32 candidate) view public returns (uint8) { require(isValid(candidate)); return votes[candidate]; } function voteFor(bytes32 candidate) public { require(isValid(candidate)); votes[candidate] += 1; } function isValid(bytes32 candidate) view public returns (bool) { return validCandidates[candidate]; } }
const fs = require('fs'); const solc = require('solc'); // npm install solc module.exports = { compile: function(path, klass) { return new Promise((resolve, reject) => { fs.readFile(path, (err, data) => { if (err) { return reject(err); } const output = solc.compile(data.toString()); const compiledContract = output.contracts[`:${klass}`]; if (compiledContract === undefined) { return reject(`no contract class "${klass}" in "${path}"`); } resolve({ abi: compiledContract.interface, bytecode: compiledContract.bytecode }); }); }); } };
const Web3 = require('web3'); const cc = require('./compile'); const provider = new Web3.providers.HttpProvider('http://localhost:7545') const contractOwner = '0x627306090abaB3A6e1400e9345bC60c78a8BEf57'; const candidates = ['Zaphod Beeblebrox', 'Foobar', 'Qux']; cc.compile('../sol/voting.sol', 'Voting').then(data => { const {abi, bytecode} = data; const web3 = new Web3(provider); const contract = new web3.eth.Contract(JSON.parse(abi)); return contract.deploy({ data: bytecode, arguments: [ candidates.map(Web3.utils.fromAscii) ] }).send({ from: contractOwner, gas: 900000, gasPrice: '20000000000' // wei }); }).then(receipt => { /* ... */ }).catch(err => { /* ... */ });
github.com/trufflesuite/ganache
Bitcoin
Ethereum
Ulteriori argomenti da approfondire: