Blockshell code modifications

Le code qui va Ăªtre prĂ©sentĂ© correspond est le code python modifiĂ© du programme Blockshell afin qu’il puisse stocker dans sa blockchain, le profile d’un Ă©tudiant EPITA.

Modification de la structure des blocks (chain.py)

blockshell/chain.py

La première modification a été apportée dans le backend du programme. Une structure Data a été créée afin de transporter les données relatives à un étudiant EPITA:

  • uid-epita -> uid
  • email-epita -> email
  • prĂ©nom -> firstname
  • nom -> secondname
  • image -> image (base64)
# ==================================================
# =================== DATA CLASS ==================
# ==================================================

# Creation d'un modele specifique de donnĂ©e qui pourra Ăªtre contenu par nos blocks
class Data:
    def __init__(self, uid, email, firstname, secondname, image):
        self.uid = uid
        self.email = email
        self.firstname = firstname
        self.secondname = secondname
        self.image = image
        
    # implementation obligatoire pour que Block puisse créer le hash à partir d'un object de classe Data
    def __str__(self):
        return str(self.uid) + str(self.email) + str(self.firstname) + str(self.secondname) + str(self.image)

Cette nouvelle structure a Ă©tĂ© créée dans le but de donner de la gĂ©nĂ©ricitĂ© au programme puisque la classe Block possède un attribut data dont le type peut varier. La seule condition implicite sur l’attribut data apparaĂ®t lors du calcul du hash du Block. L’attribut data doit pouvoir Ăªtre converti en string. C’est pour cette raison que la mĂ©thode __str__() a Ă©tĂ© implĂ©mentĂ© dans la classe Data.

# ==================================================
# =================== BLOCK CLASS ==================
# ==================================================
class Block:
    """
        Create a new block in chain with metadata
    """
    def __init__(self, data, index=0):
        self.index = index
        self.previousHash = ""
        self.data = data
        self.timestamp = str(datetime.datetime.now())
        self.nonce = 0
        self.hash = self.calculateHash()

    def calculateHash(self):
        """
            Method to calculate hash from metadata
        """
        hashData = str(self.index) + str(self.data) + self.timestamp + self.previousHash + str(self.nonce)
        return hashlib.sha256(hashData).hexdigest()

La sĂ©rialisation dans le fichier chain.txt doit donc Ăªtre ajustĂ© Ă  la gĂ©nĂ©ricitĂ© de la structure d’un bloc. Pour cela, on va s’appuyer sur la fonction getDict() qui va convertir rĂ©cursivement un objet en imbrication de dictionnaires contenant les attributs de chaque objet.

# Créer un dictionnaire récursivement à partir d'un objet
def getDict(obj):
    data = {}
    for key,value in obj.__dict__.iteritems():
        try:
            data[key] = getDict(value)
        except AttributeError:
            # si l'object n'a pas de méthode __dict__
            data[key] = value
    
    return data
# ==================================================
# ================ BLOCKCHAIN CLASS ================
# ==================================================

class Blockchain:
    # [...]

    def writeBlocks(self):
        """
            Method to write new mined block to blockchain
        """
        dataFile = file("chain.txt", "w")
        chainData = []
        for eachBlock in self.chain:
            # Pour écrire les blocks, il va falloir les lire de manière récursive du fait de notre
            # implementation de Data, contenue dans les blocks
            chainData.append(getDict(eachBlock))
        dataFile.write(json.dumps(chainData, indent=4))
        dataFile.close()

Commandes et arguments (bscli.py)

blockshell/bscli.py

Maintenant que la structure des blocks a Ă©tĂ© adaptĂ©e, il faut que l’utilisateur puisse interagir avec le programme.

Parsing de flags

Dans un premier temps, il faut une fonction capable de passer les paramètres qui seront passés aux commandes. Cette fonction prend un dictionnaire en entrée. Chaque clé du dictionnaire correspond à un flag, et chaque valeur à sa valeur par défaut. La fonction modifie le dictionnaire par référence, et return le nombre de flag récupérés dans la commande.

# fonction qui permet d'associer les flags avec leur valeur
# txData: str -> la chaine de caractère brute sans le nom de la commande
## --uid uid_value --image image_value
# options: dict -> contient tout les flags Ă  intercepter
## {'uid':'', 'image':''}
def parseArgs(txData, options):

    # Decoupe de la commande sur les balises de flags
    cmd = txData.split(' --')
    
    # On fait une copie de l'état des flags afin de déterminer à la fin de la fonction
    # Combien de paramètres on été renseignés
    options_copy = options.copy()
    
    # pour chaque string contenant le flag suivit de sa valeur séparés par un espace
    for i in range(len(cmd)):
        # on enlève les balises de flag restantes (il ne devrait plus y en avoir)
        cmd[i] = cmd[i].replace("--", '')
        # on sépare le flag de sa valeur
        cmd[i] = cmd[i].split(' ')

    # pour chaque couple [flag, value]
    for couple in cmd:
       
        if len(couple) != 2:
            print("[UNKNOWN SYNTAXE]: " + str(couple))
            continue
        
        # si le flag est intercepté par options
        if couple[0] in options:
            # on complete le champ flag du dict par sa valeur recupérée
            options[couple[0]] = couple[1]
        else:
            # sinon on affiche à l'utilisateur la non considération du flag
            print("[IGNORED]: " + couple[0] + " " + couple[1])
            
    return flagsGivenCount(options, options_copy)

CrĂ©ation d’un nouveau bloc

Une fois le parseur créé, lees nouvelles commandes peuvent Ăªtre ajoutĂ©es

Pour les besoins du sujet, une image en base64 sera générée par défaut:

flags:

  • uid
  • email
  • firstname
  • secondname
  • image
def generateRandomImage():

    array = numpy.random.rand(9,9,3) * 255

    image = Image.fromarray(array.astype('uint8')).convert('RGB')
    
    image_file = BytesIO()
    
    image.save(image_file, format='JPEG')
    image_bytes = image_file.getvalue()
    
    b_64 = base64.b64encode(image_bytes)
    return b_64

Il faut tout d’abord un commande qui puisse interpreter notre nouvelle structure de bloc, afin de les ajouter Ă  la blockchain.

# Créé un nouveau block avec le pattern d'un profil EPITA
def newblock(cmd):
    """
        Do Transaction - Method to perform new transaction on blockchain.
    """
    
    if (cmd.strip() == "newblock"):
        txData = ''
    else:
        txData = cmd.split("newblock ")[-1]
    
    # Génération d'un image aléatoire
    image_b64 = generateRandomImage()
    
    # options regroupe tous les flags que la commande s'attend Ă  retrouver, et uniquement ceux-ci
    options = {"uid":'', "email":'', "firstname":'', "secondname":'', "image":image_b64}
    if "{" in txData:
        txData = json.loads(txData)
    else:
        args = parseArgs(txData, options)
    
    print "Doing transaction..."
    
    # On créé un nouvel objet Data avec le dictionnaire des flags (passage générique des paramètres par pointeur)
    d = Data(**options)
    
    # On créé le block à partir de l'objet Data généré et on l'ajoute à la blockchain coin
    coin.addBlock(Block(data=d))

Interversion de noms entre 2 blocs

Il faut ensuite commande qui permette d’Ă©changer 2 noms. Celle-ci devra dans un premier temps vĂ©rifier la validitĂ© des champs renseignĂ©s par l’utilisateur, en s’assurant que tous les flags on Ă©tĂ© renseignĂ©s.

flags:

  • index1
  • index2
def swapnames(cmd):

    if (cmd.strip() == "swapnames"):
        txData = ''
    else:
        txData = cmd.split("swapnames ")[-1]
    
    # options regroupe tous les flags que la commande s'attend Ă  retrouver, et uniquement ceux-ci
    options = {"index1":'', "index2":''}

    # On vérifie que le nombre de paramètres renseigné est suffisant pour permettre le swap
    count_args = parseArgs(txData, options)
    if count_args != len(options):
        print("missing param for swapnames: " + str(options))
        return 1
    
    # Appel Ă  la fonction par dictionnaire
    coin.swapNames(**options)
    
    print "Doing transaction..."

La fonction swapnames() solicite le backend pour effectuer le changement. Celle-ci va confirmer la validitĂ© du changement en s’assurant que chacun des blocks identifiĂ©s dans l’interversion possède effectivement un attribut secondname. Si tel est le cas, les deux blocs sont modifiĂ©s:

Attributs modifiés sur les deux blocs:

  • data

Attributs modifiés pour tous les blocs à partir du min(index1, index2):

  • previousHash
  • timestamp
  • hash

Puis les blocks modifiés sont re-minés.

class Blockchain:
    
    # [...]

    def swapNames(self, index1, index2):
    
        # L'attribut que l'on va swap dans les blocks
        attribute = "secondname"

        intIndex1 = int(index1)
        intIndex2 = int(index2)
        sizeBlockchain = len(self.chain)

        # Rien a faire pour ce cas, la blockchain est dans son état final
        if index1 == index2:
            return 0
            
        # Filtre de validation des valeurs d'entrée
        if intIndex1 <= 0 or intIndex1 >= sizeBlockchain:
            print("unvalid index1: " + index1 + " it should be in ]0, " + str(sizeBlockchain) + "[") 
            return 1
            
        if intIndex2 <= 0 or intIndex2 >= sizeBlockchain:
            print("unvalid index2: " + index2 + " it should be in ]0, " + str(sizeBlockchain) + "[") 
            return 1

        # Récupération des blocks à swap et de leur position dans la blockchain
        metaDataBlock1 = self.findBlockWithIndex(intIndex1)
        metaDataBlock2 = self.findBlockWithIndex(intIndex2)

        # On vérifie que les blocks on bien été trouvés
        if metaDataBlock1 is None:
            print("block at " + index1 + " not found in the blockchain")
            return 1
        
        if metaDataBlock2 is None:
            print("block at " + index2 + " not found in the blockchain")
            return 1

        # Extraction des données récupérées
        pos1, block1 = metaDataBlock1
        pos2, block2 = metaDataBlock2

        # La généricité de l'attribut data de la classe Block fait que nous sommes obligés de vérifier
        # l'existance de l'attribut data.attribute (ici data.secondname)
        # de l'attribut 
        if attribute not in block1.data.__dict__:
            print(attribute + " not found in index1: " + index1 + "\n" + str(block1))
            return 1
    
        if attribute not in block2.data.__dict__:
            print(attribute + " not found in index2: " + index2 + "\n" + str(block2))
            return 1
    
        # Swap des names
        tmpAttribute = self.chain[pos1].data.__dict__[attribute]
        block1.data.__dict__[attribute] = block2.data.__dict__[attribute]
        block2.data.__dict__[attribute] = tmpAttribute
        
        # Modification de toute la blockchain à partir du premier block modifié
        # Tous les blocks positionnées en amont du premier block modifié ne changent pas
        for currentPos in range(min(pos1, pos2), len(self.chain)):
            currentBlock = self.chain[currentPos]
            previousPos = currentPos - 1
            
            currentBlock.previousHash = self.chain[previousPos].hash
            currentBlock.timestamp = str(datetime.datetime.now())
            currentBlock.hash = currentBlock.calculateHash()
            
            currentBlock.mineBlock(self.difficulty)

        # Réécriture du fichier chain.txt avec la chaine éditée
        self.writeBlocks()
        return 0

Il est Ă  prĂ©sent possible d’intervertir les noms de 2 blocs, tout en conservant les moyens de vĂ©rification de l’intĂ©gritĂ©.

Plusieurs commandes en une ligne

Afin de faciliter l’Ă©criture de scripts, il est possible de crĂ©er une commande qui permette d’embarquer plusieurs autres commandes.

Celle-ci va se contenter de parcourir l’ensemble des commandes sĂ©parĂ©es par un ‘;’ et les exĂ©cuter.

# Command qui permet de lancer plusieurs autres commandes sur une mĂªme ligne
# Utile pour le script shell
def commandlist(cmd):

    if (cmd.strip() == "commandlist"):
        txData = ''
    else:
        txData = cmd.split("commandlist ")[-1]
    
    # Chaque commande est exécutée de manière séquentielle
    for command in txData.split(';'):
        command = command.strip()
        processInput(command.strip())

Modification des fichiers .html (templates/*.html)

blockshell/templates/*.html

Récupérer les références vers les fichiers .css et .js.

Entrer l’url dans le navigateur (1):

https://getbootstrap.com/docs/4.0/getting-started/introduction/

Copier la balise link faisant référence à un fichier .css (2)
Copier les baslises script faisant référence à des fichiers .js (3):

here

Editer les fichiers:

  • guide.html
  • blockdata.html
  • blocks.html

de la façon suivante:

<!DOCTYPE html>
<html lang="en">
    <head>

        <!-- ... -->

        <!-- Bootstrap core CSS -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
        <!-- Custom styles for this template -->

        <!-- ... -->

    </head>
    <body>

        <!-- ... -->
        
        <!-- Bootstrap core JavaScript -->
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
    </body>
</html>