Squid pour Duniter, épisode 2

J’ai essayé de lancer subsquid sur la nouvelle ĞDev sans rien changer à mon docker-compose, mais ça marchait pas (pas cherché à comprendre), et comme subsquid a pas mal changé entre temps, j’ai recommencé de zéro.

Suite du sujet Subsquid for ĞDev.

  1. quickstart pour substrate https://docs.subsquid.io/quickstart/quickstart-substrate/
  2. retirer la source de données “archive” dans processor.ts (on pourra en faire une mais pour l’instant la chaîne est très courte)
  3. configurer l’endpoint RPC dans le .env pour se connecter sur notre chaîne
  4. faire une image docker https://docs.subsquid.io/deploy-squid/self-hosting/

Pour l’instant je suis bloqué là : https://substrate.stackexchange.com/questions/10502/subsquid-processor-assertion-error-in-self-hosting-tutorial

Note : j’ai publié ma première image docker sur docker hub : https://hub.docker.com/u/h30x

:timer_clock: (sera mis à jour plus tard)

J’ai un peu expérimenté avec subsquid, voilà un dépôt : Hugo Trentesaux / duniter-squid · GitLab. Quelques remarques à chaud avant des retours plus détaillés :

  • l’indexation a l’air pas mal plus lente que duniter-indexer, à confirmer et voir à quoi c’est lié → c’était le rateLimit, maintenant c’est très rapide :open_mouth:
  • subsquid a énormément changé, c’est vraiment un framework pour développer un indexeur custom, mais a priori ça ne remplit plus le rôle d’un indexeur générique exhaustif sans effort
  • un gros avantage est qu’il y a beaucoup de génération de code pour les types typescript à partir des métadonnées du runtime
  • un autre gros avantage est que c’est pensé pour prendre en compte l’évolution du runtime avec les runtime upgrade

Par contre je pense que je vais mettre en pause l’expérimentation parce que ça demande quand même un peu d’effort pour développer dans le cadre subsquid et que l’approche duniter-indexer me paraît quand même avoir un vrai intérêt.

[edit] une nuit de sommeil me permet de prendre du recul sur cette réaction à chaud après une pleine journée de boulot, je continue l’expérimentation avec les idées plus claires

2 Likes

J’ai donc continué un peu l’expérimentation sur subsquid et ça en vaut vraiment la peine. En moins d’une heure, j’ai ajouté une indexation générique de tous les calls par leur nom, l’indexation des 42000 blocs se fait en une dizaine de secondes et je suis en mesure de vous sortir la liste de tous les calls appelé depuis le début de la ĞDev (hors Timestamp.Set que j’ai exclu de l’indexation) :

Requête

query Query {
  calls {
    name
    blockNumber
  }
}

Réponse

{
  "data": {
    "calls": [
      {
        "name": "Balances.transfer",
        "blockNumber": 14435
      },
      {
        "name": "Identity.link_account",
        "blockNumber": 14646
      },
      {
        "name": "Balances.transfer_all",
        "blockNumber": 15680
      },
      {
        "name": "Balances.transfer_all",
        "blockNumber": 16128
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 30277
      },
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30287
      },
      {
        "name": "SmithMembership.revoke_membership",
        "blockNumber": 30420
      },
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30424
      },
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30504
      },
      {
        "name": "SmithMembership.request_membership",
        "blockNumber": 30516
      },
      {
        "name": "SmithMembership.claim_membership",
        "blockNumber": 30525
      },
      {
        "name": "AuthorityMembers.set_session_keys",
        "blockNumber": 30630
      },
      {
        "name": "AuthorityMembers.set_session_keys",
        "blockNumber": 30776
      },
      {
        "name": "Identity.link_account",
        "blockNumber": 30801
      },
      {
        "name": "AuthorityMembers.set_session_keys",
        "blockNumber": 30804
      },
      {
        "name": "AuthorityMembers.go_online",
        "blockNumber": 30813
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 34943
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 34960
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 34964
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 34991
      },
      {
        "name": "Balances.transfer",
        "blockNumber": 35005
      },
      {
        "name": "Cert.add_cert",
        "blockNumber": 35121
      }
    ]
  }
}

Bien entendu je peux filtrer uniquement un type de call :

query Query {
  calls(where: {name_eq: "Identity.change_owner_key"}) {
    name
    blockNumber
  }
}
{
  "data": {
    "calls": [
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30287
      },
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30424
      },
      {
        "name": "Identity.change_owner_key",
        "blockNumber": 30504
      }
    ]
  }
}

et je peux facilement ajouter les arguments du call, les événements liés…

La facilité de développement et la puissance que ça offre est déconcertante, quand je compare au temps qu’il m’a fallu pour rentrer dans l’indexeur, et réaliser des modifications.

Avantages de subsquid :

  • le code est fait pour les traitements par batch, et on peut générer une archive pour aller encore plus vite que l’indexation par RPC pour quand la blockchain sera plus grosse et plus fournie
  • le même code peut être utilisé pour l’indexation du genesis et des blocs
  • on n’a jamais à toucher à SQL, tout est derrière une couche typeorm
  • tout est nativement typé
  • tout est fait pour gérer les types substrate
  • toutes les informations des blocs, événements, calls, extrinsics, sont accessibles facilement lors de l’indexation, il n’y a pas de gymnastique pour accéder à des infos comme le timestamp

Après l’avoir découvert, j’imagine mal me passer d’un tel outil pour débugger. En deux jours j’ai découvert tous les principes de base et en une semaine je devrais être capable d’avoir redéveloppé les fonctionnalités de duniter-indexer “en mieux”.

J’aimerais quand même faire un point sur les avantages de duniter-indexer :

  • l’interface graphique hasura permet de bien comprendre la structure de la base de données et ajouter des relations sophistiquées facilement sans toucher manuellement à un schéma graphql
  • hasura permet des réglages fins sur les droits par exemple pour limiter l’utilisation de requêtes lourdes en calcul
  • le niveau de compréhension de substrate nécessaire pour développer sur l’indexeur est moindre

Pour l’instant je suis partagé entre les deux. Si je reste seul à développer l’indexeur, je préférerais travailler dans le framework subsquid. Si on était plusieurs, ça pourrait dépendre des préférences des autres.

3 Likes

Questions :

  • est-ce que subsquid a été testé sur une monnaie en prod ? Pour avoir une idée des performances avec un nombre conséquent de données
  • est-ce possible d’ajouter des query graphql plus métier ? Qui exploite les données déjà indexées ?

Oui, subsquid est utilisé sur énormément de réseaux : https://docs.subsquid.io/substrate-indexing/supported-networks/

Oui, bien sûr, le but est de coller au plus près du métier, j’ai juste exploré subsquid parce que j’avais besoin de quelque chose de générique, mais c’est tout aussi facile d’ajouter des indexations métier, je vais mettre en ligne une démo.

Qui exploite les données déjà indexées

Je vois pas ce que tu veux dire, mais subsquid intègre la migration de la base de données, donc on peut ajouter des choses sans ré-indexer depuis le début.

Je progresse à un rythme d’escargot sur docker, mais voici un indexeur subsquid basique en production : https://subsquid.gdev.coinduf.eu/graphql

Attention, ça peut casser, et pour l’instant il n’y a pas les données du genesis, mais voilà un exemple de requête :

Requête GraphQL

query MyQuery {
  transfers(limit: 10) {
    amount
    blockNumber
    from {
      id
    }
    to {
      id
    }
  }
}

Réponse JSON

{
  "data": {
    "transfers": [
      {
        "amount": "100",
        "blockNumber": 14435,
        "from": {
          "id": "5DhsQ7PCUNrTqBqiQjxKuQc1GHTn1voQkJYiUtL4H7Lhb3kj"
        },
        "to": {
          "id": "5DSF5HxiQvy2xJdtdtMSPYZdSoLAsGDxzJaifiAfAByinrH4"
        }
      },
      {
        "amount": "123",
        "blockNumber": 34943,
        "from": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        },
        "to": {
          "id": "5E6q47RRGZU15LjUiBTm2DZjpqFKAjRNafYS8YV8AzTQZtLG"
        }
      },
      {
        "amount": "456",
        "blockNumber": 34960,
        "from": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        },
        "to": {
          "id": "5EVRzJDpDxFNU9cbrGPdQD5sRsV2uiKMPNgr1sbRDX4X6Ex8"
        }
      },
      {
        "amount": "789",
        "blockNumber": 34964,
        "from": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        },
        "to": {
          "id": "5HKoFDvEQejcx4GiH6PRx93sBNPqEN6q991SZpQgHCXVUHtN"
        }
      },
      {
        "amount": "128",
        "blockNumber": 34991,
        "from": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        },
        "to": {
          "id": "5Cn8tEqF1rBGpUFJgqnadsS9AyczHePrqb9rskzATmsZTeDN"
        }
      },
      {
        "amount": "654",
        "blockNumber": 35005,
        "from": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        },
        "to": {
          "id": "5FYzKmZ2iRXXsbn9dKVZ3aFF4prdQLPpjqhBwCUUMhLcFVnD"
        }
      },
      {
        "amount": "5000",
        "blockNumber": 30277,
        "from": {
          "id": "5DSF5HxiQvy2xJdtdtMSPYZdSoLAsGDxzJaifiAfAByinrH4"
        },
        "to": {
          "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
        }
      },
      {
        "amount": "534467",
        "blockNumber": 15680,
        "from": {
          "id": "5EBW4bqGAn9uMcq5kY1tBD583ohmTVGtMUHeRxdNAcmrJZwQ"
        },
        "to": {
          "id": "5GTRwDKS984eMuvvvN1nQp5vtPAwmrGWD7rBS8QEj9ni7i2f"
        }
      },
      {
        "amount": "1247983",
        "blockNumber": 16128,
        "from": {
          "id": "5GMyvKsTNk9wDBy9jwKaX6mhSzmFFtpdK9KNnmrLoSTSuJHv"
        },
        "to": {
          "id": "5CQ8T4qpbYJq7uVsxGPQ5q2df7x3Wa4aRY6HUWMBYjfLZhnn"
        }
      }
    ]
  }
}

Pour l’instant il me manque la relation Identité ↔ Compte, mais je l’ajouterai après avoir ajouté les données du genesis.

1 Like

J’ai intégré des bouts de “giant squid” qui regroupe des implémentations spécifiques à substrate, et voilà le genre de requêtes que je peux faire maintenant :

Requête

sélectionne les blocs avec plus d’un call et affiche le numéro et la liste des calls (sauf timestamp) avec leur nom et les événements associés

query MyQuery {
  blocks(limit: 10, where: {callsCount_gt: 1}) {
    height
    calls(where: {pallet_not_eq: "Timestamp"}) {
      name
      pallet
      events {
        name
        args
      }
      success
    }
  }
}

Réponse

{
  "data": {
    "blocks": [
      {
        "height": 14435,
        "calls": [
          {
            "name": "transfer",
            "pallet": "Balances",
            "events": [
              {
                "name": "Transfer",
                "args": {
                  "to": "0x3ca979977f1bf87f5a4aafd39addcc9dfc13d895b0d072aa347811436ea97103",
                  "from": "0x48946f5c72f76c30802d2bc3b5340de53fd8c7738ce506cb47d8d17dba39fc8d",
                  "amount": "100"
                }
              }
            ],
            "success": true
          }
        ]
      },
      {
        "height": 14646,
        "calls": [
          {
            "name": "link_account",
            "pallet": "Identity",
            "events": [],
            "success": true
          }
        ]
      },
      {
        "height": 15680,
        "calls": [
          {
            "name": "transfer_all",
            "pallet": "Balances",
            "events": [
              {
                "name": "Transfer",
                "args": {
                  "to": "0xc245fb2753c79f7a7200e04f3bff10619faef085ad520721a51378edee1d157f",
                  "from": "0x5da77b50bbf86423e1fd03d129223f44d7b6bb6e58f11989322418b2e71d0067",
                  "amount": "534467"
                }
              }
            ],
            "success": true
          }
        ]
      },
      {
        "height": 16128,
        "calls": [
          {
            "name": "transfer_all",
            "pallet": "Balances",
            "events": [
              {
                "name": "Transfer",
                "args": {
                  "to": "0x0ed0734a282c8d3551694d74e12f6ec9a568770ad351a67303994908b638071b",
                  "from": "0xbe1e2dc35878f172fc1d1e89a9d15085516c16a52ddef654adcf0ee9fb9e1e72",
                  "amount": "1247983"
                }
              }
            ],
            "success": true
          }
        ]
      },
      {
        "height": 30277,
        "calls": [
          {
            "name": "transfer",
            "pallet": "Balances",
            "events": [
              {
                "name": "Transfer",
                "args": {
                  "to": "0x4e1f8a8a639370952db38fef62a763e1b8eea5600b6ccafc60d6849f028ff70f",
                  "from": "0x3ca979977f1bf87f5a4aafd39addcc9dfc13d895b0d072aa347811436ea97103",
                  "amount": "5000"
                }
              }
            ],
            "success": true
          }
        ]
      },
      {
        "height": 30287,
        "calls": [
          {
            "name": "change_owner_key",
            "pallet": "Identity",
            "events": [],
            "success": false
          }
        ]
      },
      {
        "height": 30420,
        "calls": [
          {
            "name": "revoke_membership",
            "pallet": "SmithMembership",
            "events": [],
            "success": true
          }
        ]
      },
      {
        "height": 30424,
        "calls": [
          {
            "name": "change_owner_key",
            "pallet": "Identity",
            "events": [],
            "success": false
          }
        ]
      },
      {
        "height": 30504,
        "calls": [
          {
            "name": "change_owner_key",
            "pallet": "Identity",
            "events": [],
            "success": true
          }
        ]
      },
      {
        "height": 30516,
        "calls": [
          {
            "name": "request_membership",
            "pallet": "SmithMembership",
            "events": [],
            "success": true
          }
        ]
      }
    ]
  }
}

Très puissant, et je n’ai quasiment rien eu à faire.

J’ai déjà implémenté partiellement l’indexation du genesis. C’est plus rapide que l’autre indexeur, plus facile, et j’ai pu utiliser tous les types déduits des runtime metadata et les types custom du genesis de l’autre indexeur. Franchement très agréable le typescript quand tous les types sont déjà écrits !

2 Likes

Et ça part en prod : https://subsquid.gdev.coinduf.eu/graphql

Il y a déjà pas mal de choses, plus que dans duniter-indexer, sauf quelques relations et fonctionnalités avancées de Hasura comme les tables virtuelles et la recherche full text dans les commentaires de transaction ou les pseudo.

query MyQuery {
  smithMemberships {
    identity {
      index
      name
    }
  }
}
{
  "data": {
    "smithMemberships": [
      {
        "identity": {
          "index": 34,
          "name": "cgeek"
        }
      },
      {
        "identity": {
          "index": 49,
          "name": "moul"
        }
      },
      {
        "identity": {
          "index": 58,
          "name": "vit"
        }
      },
      {
        "identity": {
          "index": 72,
          "name": "1000i100"
        }
      },
      {
        "identity": {
          "index": 344,
          "name": "HugoTrentesaux"
        }
      },
      {
        "identity": {
          "index": 1401,
          "name": "tuxmain"
        }
      },
      {
        "identity": {
          "index": 12949,
          "name": "Pini"
        }
      }
    ]
  }
}

Il y a aussi le fait de rechercher une identité par un bout du pseudo (annuaire). Faisable ?

query MyQuery {
  identities(limit: 10, where: {name_startsWith: "c"}) {
    id
  }
}
{
  "data": {
    "identities": [
      {
        "id": "candide"
      },
      {
        "id": "cgeek"
      },
      {
        "id": "cuckooland"
      },
      {
        "id": "canercandan"
      },
      {
        "id": "ccesetti"
      },
      {
        "id": "charleshd"
      },
      {
        "id": "clairecamille"
      },
      {
        "id": "cfeltz"
      },
      {
        "id": "crisb"
      },
      {
        "id": "claudia"
      }
    ]
  }
}
1 Like

Ah oui … et même :

{
  identities(limit: 10, where: {name_containsInsensitive: "c"}) {
    id
  }
}

Donc en fait les commentaires de transaction c’est déjà dans la poche, non ?

1 Like

Ça dépend ce que tu appelles “dans la poche”. Pas de problème pour les indexer, mais se pose quand même la question de savoir comment on les publie. Il faut ajouter une API quelque part qui permet d’envoyer un commentaire de transaction de manière à ce que ça soit envoyé à tous les indexeurs et reste accessible aux indexeurs qui se synchroniseront après.

D’accord, mais je ne vois pas en quoi duniter-indexer et Subsquid diffèrent là-dessus.

Oui, c’est identique. J’essayais juste de lister des inconvénients de squid par rapport à duniter-indexer et c’est pas aussi facile de faire ce genre de trucs avec squid :

(search_identity et transaction_timeserie)

Mais c’est du bonus, et peut-être que Manu trouvera quand même un moyen de brancher Hasura sur la base postgres de Squid.

J’aurais tendance à partir sur la solution avec le meilleur rapport souplesse/investissement. J’ai l’impression que dans ce cas, c’est la solution de Manu qui prend le dessus pour les premiers temps de la Ğ1v2.

Ben justement vu que l’investissement est très faible avec squid, même si la souplesse est un peu moindre, c’est ça que je privilégierais. J’ai pu mettre en place en deux jours de travail un indexeur squid qui dépasse le périmètre de duniter-indexeur, et qui réduit considérablement la charge de travail pour ajouter une fonctionnalité.

Par exemple là, si je veux l’historique des changement de statut online / offline des forgerons :

  • squid me donne accès à une requête générique que je peux faire en deux minutes
  • pour duniter-indexer, ça me demanderait une demi-journée de travail à ajouter
  • pour squid, ça me prendrait une heure d’ajouter une indexation custom si la générique ne me suffit pas

Squid répond aux deux besoins du moment :

  • aider à débugger la gdev avec des requêtes génériques
  • exposer une api simple pour les clients

Et j’ai déjà trouvé quelques :bug:bug potentiels dans Duniter en travaillant sur l’indexation squid.

1 Like

C’est un argument.

Personnellement je me dis que de toutes façons on doit viser un MVP pour les clients à la sortie de la Ğ1v2, donc pour moi Subsquid ou duniter-indexer sont deux solutions qui répondront au besoin, avec GraphQL dans les deux cas.

Donc à ce stade, c’est plus l’avis du développeur qui va bosser sur l’un ou l’autre qui m’importerait.

Manu a l’air de plus avoir très envie de se coller derrière un écran, et je suis dispo pour consacrer un peu de temps à l’indexeur, donc je vais avancer sur squid.

@flodef je te montrerai squid et tu me diras ce que tu penses de la facilité de développement dans les deux cas parce que je suis pas trop en mesure de comparer vu que je commence à avoir beaucoup de notions de substrate et une bonne connaissance de Duniter-v2 qui me donnent l’impression que tout est facile côté indexeur ><


J’ai ajouté la prise en compte de changeOwnerKey.

requête sur l'événement
query MyQuery {
  events(where: {name_eq: "IdtyChangedOwnerKey"}) {
    id
    name
    pallet
    call {
      events {
        args
        pallet
        name
      }
    }
  }
}
{
  "data": {
    "events": [
      {
        "id": "0000030504-d4b9e-000002",
        "name": "IdtyChangedOwnerKey",
        "pallet": "Identity",
        "call": {
          "events": [
            {
              "args": {
                "who": "0x3ca979977f1bf87f5a4aafd39addcc9dfc13d895b0d072aa347811436ea97103",
                "amount": "2"
              },
              "pallet": "Balances",
              "name": "Withdraw"
            },
            {
              "args": {
                "idtyIndex": 344,
                "newOwnerKey": "0x4e1f8a8a639370952db38fef62a763e1b8eea5600b6ccafc60d6849f028ff70f"
              },
              "pallet": "Identity",
              "name": "IdtyChangedOwnerKey"
            },
            {
              "args": {
                "who": "0x6d6f646c70792f74727372790000000000000000000000000000000000000000",
                "amount": "2"
              },
              "pallet": "Balances",
              "name": "Deposit"
            },
            {
              "args": {
                "tip": "0",
                "who": "0x3ca979977f1bf87f5a4aafd39addcc9dfc13d895b0d072aa347811436ea97103",
                "actualFee": "2"
              },
              "pallet": "TransactionPayment",
              "name": "TransactionFeePaid"
            }
          ]
        }
      }
    ]
  }
}
1 Like

J’ai publié la documentation “grand public” pour installer duniter-squid : https://duniter.org/wiki/duniter-v2/indexers/duniter-squid/. Si quelqu’un d’autre veut bien essayer, ça permettra d’avoir deux endpoints, de débugger l’installation, et d’améliorer le tutoriel.

J’ai aussi corrigé un bug grâce à @poka.

3 Likes