Squid pour Duniter, épisode 2

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

Je compte utiliser cette fonction pour Gecko, mais pour le moment, je n’utilise pas encore de nœud squid, et je n’ai pas vraiment envie de le faire gérer 3 types de nœuds différentes, sans compter les datapods non encore implémentés…

J’ai l’impression que Squid rempli toutes les fonctions qu’on attendrais d’un indexer clé en main, adapté pour substrate et personnalisable. Et tu sembles bien le manier désormais.

Mon avis est de lâcher l’indexer hasura pour passer sur squid, histoire de simplifier la stack et concentrer les forces.

La seule chose qui va me manquer avec squid est la fonctionnalité de recherche naturelle des noms d’identités que Manu avait intégré, ça c’est quand même très utile.
Je ne sais pas si c’est possible de migrer cette fonctionnalité vers notre squid.

Ce n’est que mon avis, peut-être qu’en creuser plus je vais m’apercevoir d’autres intérêts de garder l’indexer hasura.

1 Like

Je suis d’accord, il ne faut pas que les clients aient à gérer deux indexeurs différents. Je voulais expérimenter squid pour l’indexation générique, et je suis tombé dans le trou parce que c’était trop tentant d’implémenter un indexeur.

L’avantage de squid est la génération automatique des types à partir des runtime metadata (sqd typegen) et la génération automatique des classes typeorm à partir du schéma dans leur dialecte graphql (sqd codegen). Cela rend l’ingestion facile (les types sont connus) et le dépôt dans la base facile (pas besoin de connaître sql, juste des objets typescript).

Il faut que je discute avec Manu pour voir si on peut pas brancher Hasura sur la base postgresql-15 pour bénéficier de ce qu’il sait faire avec (tables virtuelles, fonctions sql…) mais sans utiliser la partie migrations qui est gérée par squid. Et pour l’API relay, je ne sais pas. On dirait que le système de pagination de squid ne fonctionne pas comme celui de Hasura.

Autre avantage, il y a un bug que je n’ai pas réussi à résoudre dans l’indexeur mais que j’ai réglé dans squid : l’ajout du compte avant la transaction qui le crée. C’est toujours dans les logs de l’indexeur malgré la priorisation des événements mise en place par manu, et il manque donc les transactions qui créent un compte.

2 Likes

Configurer PostgreSQL peut se faire sans Hasura, il suffit du client Pgadmin officiel ou de n’importe quel éditeur de BDD compatible Postgresql.
Je connais assez bien PostgreSQL et je peux aider.
Après je ne sais pas comment faire dans Squid pour qu’il expose les configs Postgresql maisons en GraphQL…

C’est dommage que Squid ait abandonné Hasura…(si je comprends bien).

1 Like

Pour rappel le message de Manu qui démontait la stack de subsquid:

Et ça me semble pertinent de reposter la réponse qui lui a été fait à ce sujet sur github:

Hey @ManUtopiK

First of all, thanks a lot for choosing Subsquid for indexing your Substrate data and posting this issue. We definitely considered Hasura and GraphiQL before building OpenReader. Keep in mind that Squid API is designed to be lightweight and easily deployable yet expressive, as we progress towards complete decentralization.

Here’s why:

  • We wanted a smooth and vertically integrated solution for building the schema, typescript models, migrations and GraphQL API from a single schema file.
  • We wanted to have full control of how data is fetched and what GraphQL features we want to support off the shelf. Hasura does not provide a feasible solution to the features we’d like to have. To name a few:
    • Custom scalars with full control of marshalling/unmarshalling (e.g. BigInt, Float. Address scalars)
    • JSONB queries (Hasura offers very limited filtering support)
    • Interfaces and Union types
    • Full-text search support
  • We wanted to support custom GraphQL resolvers/API extensions while keeping it simple and easily portable
  • Unified codebase (Typescript)
  • Minimal external dependencies to make decentralisation easier (remember, we’re not just building a centralized bespoke API but a decentralized marketplace for hosting Squid APIs)

Also, we don’t need an ORM with Hasura. Hasura is an ORM on top of database!
Why using typeorm in squid to build the graphql api when Hasura could do that out of the box?

I am not sure what you mean here. GraphQL server does not use TypeORM. TypeORM is indeed used under the hood by the Squid Processor, which is an ETL process which runs independently from the presentation layer.

At the same time it’s a totally legit case to use Squid API as a part of a top-level Hasura endpoint or do schema stitching any other way, if it suits you best.

I’m genuinely interested in your use-case and would be happy to discuss more in TG – feel free to ping me @dzlzv on Telegram.

1 Like

Je discute avec lui sur Telegram du coup.

Hormis le fait qu’il essai de me vendre leur solution payante de cloud ArrowSquid, voici une partie de ses réponses qui semblent confirmer les propos de @vit . Il semble aussi dire qu’on peut tout à fait remplacer le serveur graphql de squid qui est optionnel, par hasura.

The ArrowSquid solution is a decentralized data lake, where the whole historical data is compressed and distributed across a swarm of workers. This is what makes the indexing part so fast, as you don’t ingest this data from the RPC but rather from a data lake

As it only can be deployed to a decentralized network, there’s no option to self-host this part

Yet, you can use the ArrowSquid sdk using only the RPC, but it’s slow

the sdk is open-sourced

in this repo this seems to be exactly the case — RPC-only setup, which indeed you can fully self-host without integrating to the data lake (which would enable the archive: endpoint)

as for the FTS queries — yes, you can certainly do that, either by using the built-in PG capabilities or additionally pushing the data to ElasticSearch or smth else from your processor

you can also ditch our built-in graphql server and go for hasura if you want, it’s 100% optional

Moi:
Sound squid is quite flexible. thank you for this confirmation.
I don’t understand why arrowsquid swarm should offer faster solution than simple RPC setup. Do you have a technical documentation about the way ArrowSquid works ?

the data lake enables batch-queries, so you extract tens of thousands of events from hundreds of blocks in a single call, then batch-transform it. So it normally takes just a couple of minutes vs hours/days from the rpc

this is API it exposes: https://docs.subsquid.io/archives/substrate/

the rpc essentially goes through each and every block so the longer is the chain the more requests you’d need to backfill

Moi:
thank you for this informations, we will discuss about all of this :slight_smile:
I think we will try to replace you graphql server by Hasura, but that mean we need to dig how squid is build.

why? you can just spin it on top of the db, and you have full control over the schema (it’s in the migrations folder)

so integrating hasura should be very straightforward

Moi:
In fact I didn’t work on this part yet, just my friends, so yes maybe it’s just simple

let me know if you need any help!

Moi:
thank you :slight_smile:

4 Likes

Moi aussi, mais sur le channel public https://t.me/HydraDevs

En fait c’est pas tout à fait vrai qu’on a total contrôle sur la structure de la db avec squid si on veut bénéficier de la génération de code, parce qu’on doit avoir un ID string, alors que parfois on serait content d’utiliser directement des int.

Mais c’est un compromis totalement acceptable, et oui, on peut brancher Hasura dessus, pour servir les données avec un autre schéma, mais ça reste quand même assez lié aux relations qu’on choisit entre les objets.

1 Like

Aha, il se fait attaquer par une orde de juniste de tous les côtés ^^

Ok c’est bien ce que je constatais dans le json du dossier db/migrations, je ne comprennais ce que c’était que tous ces ID.

Mais il va falloir que je mette la main à la patte une bonne fois pour toute sur cette stack comme tu l’as fait pout bien comprendre, et arreter de tourner autour du pot.


Ah oui je suis en train de retracer tes questions/réponses depuis le 21 Novembre, c’est drôle ^^
En tout cas il est réactif!

Du coup ma stratégie pour les ID c’est :

  • soit j’ai besoin d’un ID spécifique, donc je mets une string qui se retrouve à partir de ce que j’aurais mis en primary key
    • identité → string(idty_index)
    • certification → string(issuer_index) + “-” + string(receiver_index)
  • soit je prends directement l’ID de l’événement qui ressemble à
    • numéro de bloc - début du hash du bloc - numéro de l’événement dans le bloc

Voilà. Simple et efficace, ça me convient comme compromis même si c’est moins élégant que les primary keys multiples qu’on avait mis dans l’autre indexeur.

Si tu veux qu’on se fasse un peu de peer coding, je crois que @flodef aussi était preneur.

1 Like

Aucun de ces deux ID ne seraient uniques, donc ce ne sont pas des ID.

Me semble bien mieux, et plus conforme à la réponse que Eldar ta fournis à ce sujet:

  1. Concerns that with interger ids it becomes possible to confuse entities with the same id, but created from a different branches of the chain.

Si, avec le modèle de données que j’ai choisi, c’est unique. Le idty_index est unique par design dans Duniter, et la certification est unique, les renouvellements sont gérés dans d’autres événements rattachés à la certification.
Il reste le problème des différentes branches, par exemple si une même certification est créée sur deux branches différentes, mais je n’ai pas encore creusé ça.
Tu peux regarder un peu le schéma que j’ai mis, ça permet ce genre de choses :

query MyQuery {
  identities(where: {name_eq: "HugoTrentesaux"}) {
    id
    index
    name
    certIssued(where: {receiver: {name_eq: "pokaa"}}) {
      id
      createdOn
      renewal {
        id
        blockNumber
      }
      receiver {
        name
        index
      }
    }
  }
}
{
  "data": {
    "identities": [
      {
        "id": "344",
        "index": 344,
        "name": "HugoTrentesaux",
        "certIssued": [
          {
            "id": "344-14053",
            "createdOn": 78505,
            "renewal": [
              {
                "id": "0000093167-d0b17-000002",
                "blockNumber": 93167
              },
              {
                "id": "0000122175-d8b13-000002",
                "blockNumber": 122175
              }
            ],
            "receiver": {
              "name": "pokaa",
              "index": 14053
            }
          }
        ]
      }
    ]
  }
}

Ça permet pour une certification (en l’occurrence de moi vers toi) de voir tous les renouvellements. Et d’ailleurs je me rends compte que j’ai mal géré l’événement NewCert, que je devrais traiter comme un renouvellement si la certification existe déjà. Mais c’est aussi un problème de l’API des événements de Duniter, ça n’a pas tellement de sens de distinguer NewCert et RenewedCert, ça pourrait être le même événement éventuellement avec un booléen “renewal”, ce serait plus clair je trouve.

J’ai re-réfléchi à ça en dormant, et je vais probablement changer le modèle de la certification pour prendre ceci en compte. En gros, chaque objet certification serait une certification continue dans le temps associée à une liste de renouvellements. Si une certification expire et est recréée, ça ferait un nouvel objet certification. Et donc le champ createdOn serait le bloc de création de la certification et expiredOn ne serait défini que si la certification a expiré. Mais je suis pas sûr. Voilà les deux choix de modèles que je propose :

Modèle certification unique

On a une unique instance d’une certification de A→B, associée à une liste de créations, renouvellements, expirations. Il peut y avoir des discontinuités, c’est-à-dire des périodes où la certification n’est plus valide entre une expiration et la re-création qui la suit.

Modèle de certification continue

On a une instance de certification par période de validité continue. Donc on peut avoir une instance A→B pour la période 1 avec des renouvellements, puis une nouvelle instance de la certification A→B pour la période 2 avec encore des renouvellements.

J’aimerais des avis là dessus, pour éviter d’implémenter quelque chose à chaque fois que j’ai une nouvelle idée et de jamais stabiliser :smiley:


Par rapport à l’identité, l’id pourrait être celui de l’événement de création d’identité.

1 Like

En effet, je suis chaud. J’avais plein de choses à gérer en off, mais c’est toujours un plaisir de passer un moment productif et rigolo ensemble. Joindre l’utile à l’agréable :smiley:

J’ai donc implémenté ça :

query MyQuery {
  identities(where: {name_eq: "HugoTrentesaux"}) {
    name
    certIssued(where: {createdOn_not_eq: 0}) {
      id
      active
      createdOn
      expireOn
      issuer {
        name
        id
      }
      receiver {
        name
        id
      }
      creation {
        id
      }
      renewal {
        id
      }
      removal {
        id
      }
    }
  }
}
{
  "data": {
    "identities": [
      {
        "name": "HugoTrentesaux",
        "certIssued": [
          {
            "id": "0000035121-7082e-000002",
            "active": true,
            "createdOn": 35121,
            "expireOn": 2137521,
            "issuer": {
              "name": "HugoTrentesaux",
              "id": "genesis-344"
            },
            "receiver": {
              "name": "cgeek",
              "id": "genesis-34"
            },
            "creation": [
              {
                "id": "0000035121-7082e-000002"
              }
            ],
            "renewal": [],
            "removal": []
          },
          {
            "id": "0000078505-0d84f-000003",
            "active": true,
            "createdOn": 78505,
            "expireOn": 2224575,
            "issuer": {
              "name": "HugoTrentesaux",
              "id": "genesis-344"
            },
            "receiver": {
              "name": "pokaa",
              "id": "0000078505-0d84f-000002"
            },
            "creation": [
              {
                "id": "0000078505-0d84f-000003"
              }
            ],
            "renewal": [
              {
                "id": "0000093167-d0b17-000002"
              },
              {
                "id": "0000122175-d8b13-000002"
              }
            ],
            "removal": []
          }
        ]
      }
    ]
  }
}

On voit que :

  • chaque certification A→B n’existe qu’en un seul exemplaire, mais avec createdOn et expireOn actualisés
  • on peut savoir si une certification est active avec le champ “active”
  • les id des certifications sont uniques et correspondent (sauf genesis) à l’event id de l’événement de création de la certification
  • les id des identités correspondent (sauf genesis) à l’event id de l’événement de création d’identité
  • à chaque certification est associée sa liste de création, renouvellement, suppression

Cela veut dire que si une certification est créée, puis supprimée, puis recréée, son id sera quand même celui de l’événement de la première création. Est-ce que ça te semble mieux @poka ?

Il faut juste que je répare le son sur mon ordi sinon ça va pas être pratique :slight_smile:
Tu as suivi un peu sur le forum, quand même ? Il s’est passé pas mal de choses.

1 Like

Dites moi quand, j’en serais :wink:

1 Like

J’essaie de voir tous les calls go_online et go_offline depuis le début.
Je n’arrive pas à obtenir l’adresse ss58 de l’émetteur du call.

query Query {
  calls(where: {pallet_not_eq: "Timestamp", name_startsWith: "go_"}, orderBy: id_DESC) {
    name
    block {
      height
      timestamp
    }
    pallet
  }
}

Résultat:

{
  "data": {
    "calls": [
      {
        "name": "go_online",
        "block": {
          "height": 10716,
          "timestamp": "2023-11-30T15:32:42.001000Z"
        },
        "pallet": "AuthorityMembers"
      },
      {
        "name": "go_online",
        "block": {
          "height": 540,
          "timestamp": "2023-11-29T22:32:54.002000Z"
        },
        "pallet": "AuthorityMembers"
      }
    ]
  }
}

Je ne vois que 2 go_online et aucun go_offline, je pensais à un bug mais en fait ça doit être ça.

Il y a un bug sur l’indexeur que j’ai vu hier :

  • j’ai créé une identité pour bgallois, mais son compte n’existait pas
  • donc l’indexeur plante parce qu’il essaye de référencer un compte qui n’a pas eu d’événement System.NewAccount

Et donc actuellement les données subsquid sont à la bourre.
Je vais corriger ce bug parce que c’est handicapant.

Pour l’instant tu ne peux qu’avoir l’adresse en hexadécimal :

query MyQuery {
  calls(orderBy: block_id_DESC, limit: 10, where: {name_startsWith: "go_"}) {
    pallet
    name
    extrinsic {
      signature {
        address
      }
    }
  }
}
{
  "data": {
    "calls": [
      {
        "pallet": "AuthorityMembers",
        "name": "go_online",
        "extrinsic": {
          "signature": {
            "address": {
              "value": "0x5cc2d273760c01d1030c5c5bb3966c6639914af56e06742369c9bcfdef4bf5a5",
              "__kind": "Id"
            }
          }
        }
      },
      {
        "pallet": "AuthorityMembers",
        "name": "go_online",
        "extrinsic": {
          "signature": {
            "address": {
              "value": "0x4e1f8a8a639370952db38fef62a763e1b8eea5600b6ccafc60d6849f028ff70f",
              "__kind": "Id"
            }
          }
        }
      }
    ]
  }
}

Parce que la conversion en ss58 doit se faire explicitement (comme pour les Account). C’est ici :

src/main.ts · 20befebced57f0382ea732f4647245b93daab1b8 · Hugo Trentesaux / duniter-squid · GitLab

Mais théoriquement on pourrait modifier le code généré automatiquement pour avoir les adresses au format ss58 dans les signatures aussi :
src/model/generated/_extrinsicSignature.ts · 20befebced57f0382ea732f4647245b93daab1b8 · Hugo Trentesaux / duniter-squid · GitLab

1 Like

Je viens de regarder vite fait, ils ont viré Hasura j’ai l’impression. Du coup mes remarques sur le fait que c’était monté à l’envers ne tiennent plus.
En regardant le code de @HugoTrentesaux, ça a l’air moins usine à gaz qu’avant.

Hasura n’a besoin que d’une bdd postgresql, donc c’est toujours possible…

4 Likes

Btw, j’ai ajouté les certifications forgeron, ce sera plus pratique pour l’historique :

query MyQuery {
  smithCertCreations {
    cert {
      issuer {
        name
        account {
          id
        }
      }
      receiver {
        name
        account {
          id
        }
      }
    }
    blockNumber
  }
  smithCertRenewals {
    cert {
      issuer {
        name
        account {
          id
        }
      }
      receiver {
        name
        account {
          id
        }
      }
    }
    blockNumber
  }
}
{
  "data": {
    "smithCertCreations": [
      {
        "cert": {
          "issuer": {
            "name": "cgeek",
            "account": {
              "id": "5E6q47RRGZU15LjUiBTm2DZjpqFKAjRNafYS8YV8AzTQZtLG"
            }
          },
          "receiver": {
            "name": "poka",
            "account": {
              "id": "5CQ8T4qpbYJq7uVsxGPQ5q2df7x3Wa4aRY6HUWMBYjfLZhnn"
            }
          }
        },
        "blockNumber": 19582
      },
      {
        "cert": {
          "issuer": {
            "name": "HugoTrentesaux",
            "account": {
              "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
            }
          },
          "receiver": {
            "name": "poka",
            "account": {
              "id": "5CQ8T4qpbYJq7uVsxGPQ5q2df7x3Wa4aRY6HUWMBYjfLZhnn"
            }
          }
        },
        "blockNumber": 22025
      },
      {
        "cert": {
          "issuer": {
            "name": "cgeek",
            "account": {
              "id": "5E6q47RRGZU15LjUiBTm2DZjpqFKAjRNafYS8YV8AzTQZtLG"
            }
          },
          "receiver": {
            "name": "bgallois",
            "account": {
              "id": "5F6xAX1k6eRKUGrF7exifKcS2K2SB781Cn6soV1kahjwkGpg"
            }
          }
        },
        "blockNumber": 65384
      },
      {
        "cert": {
          "issuer": {
            "name": "HugoTrentesaux",
            "account": {
              "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
            }
          },
          "receiver": {
            "name": "bgallois",
            "account": {
              "id": "5F6xAX1k6eRKUGrF7exifKcS2K2SB781Cn6soV1kahjwkGpg"
            }
          }
        },
        "blockNumber": 65487
      }
    ],
    "smithCertRenewals": [
      {
        "cert": {
          "issuer": {
            "name": "HugoTrentesaux",
            "account": {
              "id": "5Dq8xjvkmbz7q4g2LbZgyExD26VSCutfEc6n4W4AfQeVHZqz"
            }
          },
          "receiver": {
            "name": "moul",
            "account": {
              "id": "5HKoFDvEQejcx4GiH6PRx93sBNPqEN6q991SZpQgHCXVUHtN"
            }
          }
        },
        "blockNumber": 80938
      }
    ]
  }
}
1 Like