Revamping IoT con Blockchain: un dimostratore Smart Contract per anti-tampering
Quando un'attività viene periodicamente eseguita e registrata online, cosa potrebbe fare il gestore del servizio per garantire che non modificherà i dati storici?
Descrizione del problema
Un'attività viene periodicamente eseguita e registrata: per esempio, un insieme di sensori raccoglie dei dati fisici e li memorizza su di un server, esponendo pubblicamente i dati raccolti tramite una pagina web e un webservice JSON
.
Questo sistema ha un punto centralizzato per salvare e mostrare i dati. Cosa potrebbe modificare il gestore del servizio per garantire che non altererà i dati storici?
Soluzioni scartate
- Una prima idea è basata su file: firmando periodicamente, con cifratura asimmetrica, dei blocchi di dati raccolti, per poi mandarli a un servizio di marcatura temporale. Potrebbe funzionare ma è scomoda la verifica che non ci sia stato tampering rispetto al webservice;
- una soluzione alternativa è di utilizzare IPFS costruendo una catena di file relativi ai vari blocchi di dati: sarebbero immutabili ma senza date;
- si potrebbero usare soluzioni pronte basate su blockchain: ci sono OpenTimestamps e originstamp; la prima è gratuita ma molto laboriosa, mentre la seconda è semplice ma troppo cara per piccole realtà.
La soluzione proposta
Il Proof Of Concept qui descritto potrebbe essere una soluzione semplice e a portata di piccoli progetti.
Descriviamo brevemente i passi del processo nell'architettura del diagramma precedente.
- I dati raccolti dai sensori vengono memorizzati in una tabella di un DB e resi disponibili in lettura tramite sia un webservice che una pagina web;
- periodicamente, blocchi di questi dati vengono serializzati (per esempio in un
JSON
) e di questa stringa viene calcolato un hash; - tramite un client web3, l'hash viene inviato in scrittura a uno smart contract;
- lo smart contract memorizza nel proprio stato un hash calcolato sulla concatenazione di due stringhe: l'hash di stato e l'hash in ingresso (si veda il diagramma seguente);
- l'hash risultante viene memorizzato come nuovo hash di stato (che, alla creazione dello smart contract, sarà inizializzato da una stringa nota, per esempio nulla).
Ogni transazione sulla blockchain ha un timestamp non modificabile da chi raccoglie i dati e, se questi ultimi venissero alterati, l'hash dell'algoritmo descritto, partendo dai dati del webservice centralizzato, sarebbe diverso da quello memorizzato nello smart contract.
È di fatto una applicazione di un caso particolare di Merkle tree sbilanciato ed è anche robusto alle variazioni del JSON
(si decidesse per esempio di aumentare il numero di campi sul DB) perché quel che viene gestito è un hash, che è indifferente alla semantica della stringa da lui elaborata.
Codice
Il codice è disponibile su GitHub come atpoc (anti tampering (smart contract) proof of concept) e a quel repository si deve fare riferimento: sono qui riportate solo le parti essenziali per l'articolo stesso, mentre su GitHub si trovano anche altri metodi (per le transazioni, per esempio).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;
import "../node_modules/@openzeppelin/contracts/utils/Strings.sol";
contract Mlist {
address owner;
uint hashInternal;
constructor(string memory _hashString) {
owner = msg.sender;
hashInternal = uint(keccak256(abi.encodePacked(_hashString)));
}
function concatenate(string memory a, string memory b) internal pure returns (string memory) {
return string(abi.encodePacked(a, b));
}
function gethashInternalAsString() public view returns(string memory) {
return Strings.toHexString(hashInternal);
}
function sethashInternal(string memory _hashString) public {
require(msg.sender == owner, "Not the owner");
string memory hashInternalString = Strings.toHexString(hashInternal);
string memory stringNew = concatenate(hashInternalString, _hashString);
hashInternal = uint(keccak256(abi.encodePacked(stringNew)));
}
}
Il semplice smart contract in Solidity realizza quanto descritto, consentendo le operazioni in scrittura sull'hash interno solo da parte di chi ha pubblicato il contratto.
let Mlist = artifacts.require("Mlist")
module.exports = (deployer) => {
deployer.deploy(Mlist, "")
}
Il codice di inizializzazione imposta l'hash interno al valore equivalente a quello dato da una stringa vuota.
const truffleAssert = require('truffle-assertions')
const Mlist = artifacts.require("Mlist")
contract('Mlist', (accounts) => {
const aString = '0x3ac225168df54212a25c1c01fd35bebfea408fdac2e31ddd6f80a4bbf9a5f1cb' // 'a'
let mList
before(async () => {
mList = await Mlist.deployed()
})
it('It should return 0xc5d...470', async () => {
const hashInternalAsString = await mList.gethashInternalAsString()
const emptyStringHash = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'
assert.equal(hashInternalAsString, emptyStringHash)
})
it('It should set hashInternal to 0xbea...3ae', async() => {
const combinedString = '0xbea7774072254bb0d224ea4ea5daeabc6b2520620334696fde6408429355e3ae' // void + 'a'
await mList.sethashInternal(aString, {from: accounts[0]})
const hashInternalAsString = await mList.gethashInternalAsString()
assert.equal(hashInternalAsString, combinedString)
})
it("It should abort with an error", async () => {
await truffleAssert.reverts(mList.sethashInternal(aString, {from: accounts[1]}), "Not the owner")
})
})
I test unitari verificano le proprietà di base, compreso il fallimento per il tentativo di scrittura da parte di indirizzi diversi (l'array di account a cui si riferiscono i test è quello del classico ambiente di sviluppo locale basato su Truffle e Ganache, con l'aggiunta di comode librerie come OpenZeppelin Contracts e truffle-assertions).
Conclusioni e note
Come scritto, può essere una soluzione Proof of Concept per piccoli archivi. I principali temi che andrebbero ancora sviluppati o i problemi che si presentano, sono questi:
- non è una soluzione completamente decentralizzata, perché il gestore del server può per esempio creare e usare un nuovo smart contract contenente l'hash di dati “truccati”, indicandolo sul sito in sostituzione del precedente: in questo caso, uno smart contract oracle indipendente potrebbe vigilare (tema che meriterebbe una trattazione separata);
- non è stato descritto il client web3 necessario per la comunicazione tra il sistema legacy e lo smart contract;
- lo smart contract, per brevità didattica, non espone un metodo per il trasferimento degli eventuali ETH a lui inviati per sbaglio e, in questo caso, sarebbero resi indefinitamente indisponibili;
- si sarebbe potuto scrivere il contratto in Vyper e non in Solidity.
--
Photo by Shubham Dhage on Unsplash