Blockchain for Developers

Software Developers Arena

16 February 2018

Sergio Perticone

Skillbill srl

Distributed ledger

È un database distribuito di transazioni

Un po' come BitTorrent, ma più delicato:

Spauracchio del sistema: double spending

1. Bitcoin

Satoshi Nakamoto's Bitcoin

Ottobre 2008:

Fondamenta teoriche della blockchain

Rilascio del codice e live da gennaio 2009

Bitcoin P2P network

Ad alto livello:

Blockchain Internals

Block

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
}
struttura del blocco, src/toychain/block.go

PrevHash è ciò che forma la chain

Miner

I miner sono i peer che producono i blocchi
Qualunque peer può essere il miner di un blocco

Incentivi:

Qual è il trucco?

Proof of Work

Proof of Work

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)

Proof of Work

Grazie alla PoW:

Proof of Work di Hashcash

Bitcoin ha mutuato la PoW da Hashcash [Dwork, Naor 1992]

Idea: il francobollo per la posta elettronica

Ricorda qualcosa?

Hashcash

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
}
header, src/cmd/hashcash.go

Hashcash

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"

Non è triviale, computazionalmente, perché SHA-1 è una funzione di hash crittografica

Hashcash

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

Proof of Work nella Blockchain

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
}
goroutine che mina un blocco, src/toychain/miner.go

Verifica della PoW

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
}
verifica della PoW, src/toychain/chain.go

Proof of Stake

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

Transazioni

Transazione

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
}
transazione, src/toychain/tx.go

UTXO

I'oggetto di una transazione sono gli UTXO

type UTXO struct {
    OutputIndex int
    TxIndex     int
    BlockIndex  int
}
unspent transaction output, src/toychain/utxo.go

UTXO Set

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
UTXO Set, src/toychain/utxo.go

UTXO

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
}
token dagli utxo, src/toychain/chain.go

UTXO

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,
            })
        }
        // ...
snippet trasferimento utxo, src/toychain/chain.go

Indirizzi

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)
}
generazione indirizzo, src/toychain/crypto/crypto.go

Esempi di indirizzi:

Transazioni

Alice vuole pagare token a Bob:

I token trasferiti saranno spendibili solo da chi possiede la chiave privata di Bob

Transazioni

meccanismo crittografico delle le transazioni

PayToAddress

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
}

Transazioni

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
}
verifica di una transazione, src/toychain/chain.go

Transazioni

Per evitare il double spending, sono previste le seguenti limitazioni:

2. Smart Contracts

Smart Contracts

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:

Smart Contract

Possiamo usare la blockchain?

Bitcoin scripting

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

Bitcoin VM

Ogni nodo esegue una VM

Un transazione viene effettuata tramite l'esecuzione di uno script

Bitcoin VM

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)
    ...
    }
}

Script

Gli script per Bitcoin sono un'espressione RPN della forma:

[ DATO... ] [ DATO | OPCODE ]...
   input       programma

P2PKH

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

Script alternativi

$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

Limitazioni

Gli Smart Contract su bitcoin non sembrano poi così "smart"

Ethereum

Ethereum

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)

Accounts

Non ci sono UTXO

Ad un account Ethereum è invece associato uno stato:

Un transazione corrisponde ad un cambio di stato

Accounts

Esistono due tipi di account

Esterni

Contratti

Indirizzo

160 bit meno significativi dell'hash della chiave pubblica

indirizzo = SHA-3(pubKey) & (1<<160) - 1

Esempio: 0x627306090abaB3A6e1400e9345bC60c78a8BEf57

EVM

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

ether

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

Solidity

Il linguaggio "contract-oriented" per lo sviluppo di Smart Contract su Ethereum

Solidity

Does anybody remember DAO?

Alcuni problemi di design:

In generale sono problemi ben affrontati in letteratura e molti risolti in linguaggi general purpose

Solidity

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];
  }
}

Compilazione contratto

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
                });
            });
        });
    }
};

Deploy sulla blockchain

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 => {
    /* ... */
});

Ganache

Voting DAPP

dapp/voting.html

Conclusioni

Bitcoin

Ethereum

Ulteriori argomenti da approfondire:

Thank you

Sergio Perticone

Skillbill srl