Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1000i100-test
  • 105_gitlab_container_registry
  • cgeek/issue-297-cpu
  • ci_cache
  • debug/podman
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-smoldot
  • feature/dc-dump
  • feature/distance-rule
  • feature/show_milestone
  • fix-252
  • gdev-800-tests
  • hugo-release/runtime-701
  • hugo-tmp-dockerfile-cache
  • hugo/195-doc
  • hugo/195-graphql-schema
  • hugo/distance-precompute
  • hugo/endpoint-gossip
  • hugo/tmp-0.9.1
  • master
  • network/gdev-800
  • network/gdev-802
  • network/gdev-803
  • network/gdev-900
  • pini-check-password
  • release/client-800.2
  • release/hugo-chainspec-gdev5
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • release/runtime-401
  • release/runtime-500
  • release/runtime-600
  • release/runtime-700
  • release/runtime-701
  • release/runtime-800
  • tests/distance-with-oracle
  • tuxmain/anonymous-tx
  • tuxmain/benchmark-distance
  • update-docker-compose-rpc-squid-names
  • gdev-800
  • gdev-800-0.8.0
  • gdev-802
  • gdev-803
  • gdev-900-0.10.0
  • gdev-900-0.10.1
  • gdev-900-0.9.0
  • gdev-900-0.9.1
  • gdev-900-0.9.2
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • runtime-401
  • runtime-500
  • runtime-600
  • runtime-700
  • runtime-701
  • runtime-800
  • runtime-800-backup
  • runtime-800-bis
  • runtime-801
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
  • v0.4.1
80 results

Target

Select target project
  • nodes/rust/duniter-v2s
  • llaq/lc-core-substrate
  • pini-gh/duniter-v2s
  • vincentux/duniter-v2s
  • mildred/duniter-v2s
  • d0p1/duniter-v2s
  • bgallois/duniter-v2s
  • Nicolas80/duniter-v2s
8 results
Select Git revision
  • archive_upgrade_polkadot_v0.9.42
  • david-wot-scenarios-cucumber
  • distance
  • elois-ci-binary-release
  • elois-compose-metrics
  • elois-duniter-storage
  • elois-fix-85
  • elois-fix-idty-post-genesis
  • elois-fix-sufficients-change-owner-key
  • elois-opti-cert
  • elois-remove-renewable-period
  • elois-revoc-with-old-key
  • elois-rework-certs
  • elois-smish-members-cant-change-or-rem-idty
  • elois-smoldot
  • elois-substrate-v0.9.23
  • elois-technical-commitee
  • hugo-gtest
  • hugo-remove-duniter-account
  • hugo-rework-genesis
  • hugo-tmp
  • jrx/workspace_tomls
  • master
  • no-bootnodes
  • pallet-benchmark
  • release/poka-chainspec-gdev5
  • release/poka-chainspec-gdev5-pini-docker
  • release/runtime-100
  • release/runtime-200
  • release/runtime-300
  • release/runtime-400
  • test-gen-new-owner-key-msg
  • ts-types
  • ud-time-64
  • upgrade_polkadot_v0.9.42
  • runtime-100
  • runtime-101
  • runtime-102
  • runtime-103
  • runtime-104
  • runtime-105
  • runtime-200
  • runtime-201
  • runtime-300
  • runtime-301
  • runtime-302
  • runtime-303
  • runtime-400
  • v0.1.0
  • v0.2.0
  • v0.3.0
  • v0.4.0
52 results
Show changes
Showing
with 1444 additions and 477 deletions
Feature: Balance transfer
Feature: Account creation
Scenario: Create a new account with enough funds
When alice sends 5 ĞD to dave
Then dave should have 5 ĞD
When 1 block later
"""
The blockchain should automatically withdraw account creation tax (3 ĞD)
The blockchain did not automatically withdraw account creation tax (3 ĞD) because this feature has been removed
"""
Then dave should have 2 ĞD
Then dave should have 5 ĞD
Scenario: Create a new account without enough funds then retry with enough funds
When alice sends 2 ĞD to eve
Then eve should have 2 ĞD
When 1 block later
"""
The blockchain should automatically destroy Eve account
because Eve does not have enough funds to pay the new account tax
The blockchain did not automatically destroy Eve account for Eve not having enough funds to pay the new account tax
Because this feature has been removed
"""
Then eve should have 0 ĞD
Then eve should have 2 ĞD
When alice send 5 ĞD to eve
Then eve should have 5 ĞD
Then eve should have 7 ĞD
When 1 block later
"""
The blockchain should automatically withdraw account creation tax (3 ĞD)
The blockchain did not automatically withdraw account creation tax (3 ĞD) because this feature has been removed
"""
Then eve should have 2 ĞD
Then eve should have 7 ĞD
@ignoreErrors
Scenario: Create a new account without any funds
Then eve should have 0 ĞD
# Alice is treasury funder for 1 ĞD
Then alice should have 9 ĞD
When eve send 0 ĞD to alice
Then alice should have 10 ĞD
Then alice should have 9 ĞD
When alice send 5 ĞD to eve
Then eve should have 5 ĞD
When 1 block later
"""
The blockchain should automatically withdraw account creation tax (3 ĞD)
The blockchain did not automatically withdraw account creation tax (3 ĞD) because this feature has been removed
"""
Then eve should have 2 ĞD
Then eve should have 5 ĞD
@genesis.bad_distance
Feature: Distance fail
#
# WoT:
#
# H<->G<->E<->D<->C-->B
# ^ ^ ^ ^
# \ / \ /
# v v v v
# I A
#
# Every member is referee. Referee count = 8; 80% = 6.4
# Certs from Alice and Bob do not ensure the fulfilling of the distance rule
# because the newcomer would reach only 6 members up to G.
Scenario: an unvalidated member fails the distance rule
Then treasury should contain 1 ĞD
When alice sends 7 ĞD to ferdie
Then alice should have 0 ĞD reserved
Then alice should have 199 cĞD
When bob sends 750 cĞD to ferdie
When 15 block later
When alice creates identity for ferdie
Then ferdie identity should be unconfirmed
Then ferdie should be certified by alice
When ferdie confirms his identity with pseudo "ferdie"
Then ferdie identity should be unvalidated
When 3 block later
When bob certifies ferdie
Then ferdie should be certified by bob
Then ferdie should have 0 ĞD reserved
# 700 + 750 - 2(one transaction fee, not a member yet)
Then ferdie should have 1448 cĞD
When ferdie requests distance evaluation
Then ferdie should have 10 ĞD reserved
# 1448 - 1000 - 2(one transaction fee, not a member yet)
Then ferdie should have 446 cĞD
When 7 blocks later
Then treasury should contain 105 cĞD
When alice runs distance oracle
When 7 blocks later
Then ferdie should be certified by alice
Then ferdie should be certified by bob
# The distance rule is failed
Then ferdie identity should be unvalidated
# Ferdie got his reserve slashed
Then ferdie should have 0 ĞD reserved
Then ferdie should have 446 cĞD
# Slashed amount is transfered to treasury
Then treasury should contain 1105 cĞD
......@@ -2,14 +2,37 @@ Feature: Identity creation
Scenario: alice invites a new member to join the web of trust
# 6 ĞD covers:
# - account creation fees (3 ĞD)
# - existential deposit (2 ĞD)
# - existential deposit (1 ĞD)
# - transaction fees (below 1 ĞD)
When alice sends 6 ĞD to ferdie
When alice sends 7 ĞD to dave
# Alice is treasury funder for 1 ĞD => 10-1-7 = 2 (minus TODO fees)
Then alice should have 0 ĞD reserved
Then alice should have 199 cĞD
When bob sends 750 cĞD to dave
When charlie sends 6 ĞD to eve
# alice last certification is counted from block zero
# then next cert can be done after cert_period
# then next cert can be done after cert_period, which is 15
When 15 block later
When alice creates identity for ferdie
Then ferdie identity should be created
When ferdie confirms his identity with pseudo "Ferdie"
Then ferdie identity should be confirmed
When alice creates identity for dave
Then dave identity should be unconfirmed
Then dave should be certified by alice
When dave confirms his identity with pseudo "dave"
Then dave identity should be unvalidated
When 3 block later
When bob certifies dave
When charlie certifies dave
Then dave should be certified by bob
Then dave should be certified by charlie
Then dave should have 0 ĞD reserved
# 700 + 750 - 2(one transaction fee, not a member yet)
Then dave should have 1448 cĞD
When dave requests distance evaluation
Then dave should have 10 ĞD reserved
# 1448 - 1000 - 2(one transaction fee, not a member yet)
Then dave should have 446 cĞD
When 7 blocks later
When alice runs distance oracle
When 7 blocks later
Then dave identity should be member
Then dave should have 0 ĞD reserved
Then dave should have 1446 cĞD
Feature: Balance transfer
Feature: Monetary mass
Scenario: After 10 blocks, the monetary mass should be 60 ĞD
Then Monetary mass should be 30.00 ĞD
Then Current UD amount should be 10.00 ĞD
When 10 blocks later
When 15 blocks later
Then Monetary mass should be 60.00 ĞD
When 10 blocks later
Then Monetary mass should be 90.00 ĞD
......
Feature: Oneshot account
Scenario: Simple oneshot consumption
When charlie sends 7 ĞD to dave
# Cover the oneshot calls fees
When alice sends 7 ĞD to oneshot dave
Then alice should have 3 ĞD
# Alice is treasury funder for 1 ĞD, and member so fees are refunded
Then alice should have 2 ĞD
Then dave should have oneshot 7 ĞD
When oneshot dave consumes into account bob
Then dave should have oneshot 0 ĞD
Then bob should have 1699 cĞD
Then bob should have 1698 cĞD
Then bob should have oneshot 0 ĞD
Scenario: Double oneshot consumption
When charlie sends 7 ĞD to dave
Then charlie should have 299 cĞD
# Cover the oneshot calls fees
When alice sends 7 ĞD to oneshot dave
Then alice should have 3 ĞD
# Alice is treasury funder for 1 ĞD, and member so fees are refunded
Then alice should have 2 ĞD
Then dave should have oneshot 7 ĞD
When oneshot dave consumes 4 ĞD into account bob and the rest into oneshot charlie
Then dave should have oneshot 0 ĞD
Then bob should have 14 ĞD
Then bob should have oneshot 0 ĞD
Then charlie should have 10 ĞD
Then charlie should have oneshot 299 cĞD
Then charlie should have 299 cĞD
Then charlie should have oneshot 298 cĞD
......@@ -2,13 +2,16 @@
Feature: Balance transfer all
Scenario: If bob sends all his ĞDs to Dave
When bob sends all her ĞDs to dave
When bob sends all his ĞDs to dave
"""
Bob is a member, as such he is not allowed to empty his account completely,
if he tries to do so, the existence deposit (2 ĞD) must remain.
if he tries to do so, the existence deposit (1 ĞD) must remain.
Bob is a member, transaction fees are refunded for him
101 = existential deposit (100) + fees refunded using quota (001)
"""
Then bob should have 2 ĞD
Then bob should have 101 cĞD
"""
10 ĞD (initial Bob balance) - 2 ĞD (Existential deposit) - 0.02 ĞD (transaction fees)
10 ĞD (initial Bob balance) - 1 ĞD (Existential deposit) - 0.02 ĞD (transaction fees)
"""
Then dave should have 798 cĞD
Then dave should have 898 cĞD
# TODO check that the missing cent went to treasury
@genesis.wot
Feature: Universal Dividend
Scenario: Eligibility at genesis
When 2 blocks later
# Members
Then alice should be eligible to UD
Then bob should be eligible to UD
Then charlie should be eligible to UD
# Not members
Then eve should not be eligible to UD
Then ferdie should not be eligible to UD
{
"first_ud": null,
"first_ud_reeval": null,
"genesis_parameters": {
"genesis_certs_expire_on": 1000,
"genesis_certs_min_received": 2,
"genesis_memberships_expire_on": 1000,
"genesis_smith_certs_expire_on": 1000,
"genesis_smith_certs_min_received": 2,
"genesis_smith_memberships_expire_on": 100000
},
"identities": {
"Alice": {
"index": 1,
"balance": 1000,
"certs_received": {
"Bob": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Bob": {
"index": 2,
"balance": 1000,
"certs_received": {
"Alice": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Charlie": {
"index": 3,
"balance": 1000,
"certs_received": {
"Alice": 2700000000,
"Dave": 2700000000
},
"owner_address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Dave": {
"index": 4,
"balance": 1000,
"certs_received": {
"Charlie": 2700000000,
"Eve": 2700000000
},
"owner_address": "5EWtNo12S57725GF641a6avLkHaXMWnJ5RPoo721GR92gSEt",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Eve": {
"index": 5,
"balance": 1000,
"certs_received": {
"Dave": 2700000000,
"Gertrude": 2700000000
},
"owner_address": "5EjiXi8p1sCEUevSCQkHom6yUFQY9N5U5xpEofE2N4ZhJ9vs",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Gertrude": {
"index": 6,
"balance": 1000,
"certs_received": {
"Eve": 2700000000,
"Henry": 2700000000,
"Irene": 2700000000
},
"owner_address": "5Hgx76GUW5ETtxTX53BJqzeRtxTASC4AgCPwGDuUjvSuKbC6",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Henry": {
"index": 7,
"balance": 1000,
"certs_received": {
"Irene": 2700000000,
"Gertrude": 2700000000
},
"owner_address": "5DJZ3Ns49G7B8tr2av53WZorSsMLzvfhPPVeS2RPispMK3Y9",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Irene": {
"index": 8,
"balance": 1000,
"certs_received": {
"Henry": 2700000000,
"Gertrude": 2700000000
},
"owner_address": "5EqdTcg58VmBfd7DGPj1GJeHahqUEh3fjWH2fS4KRQmhpAjN",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
}
},
"parameters": {
"babe_epoch_duration": 30,
"cert_period": 15,
"cert_max_by_issuer": 10,
"cert_min_received_cert_to_issue_cert": 2,
"cert_validity_period": 1000,
"idty_confirm_period": 40,
"idty_creation_period": 50,
"membership_period": 1000,
"membership_renewal_period": 500,
"ud_creation_period": 60000,
"ud_reeval_period": 600000,
"smith_cert_max_by_issuer": 8,
"smith_inactivity_max_duration": 48,
"smith_wot_min_cert_for_membership": 2,
"wot_first_cert_issuable_on": 20,
"wot_min_cert_for_create_idty_right": 2,
"wot_min_cert_for_membership": 2
},
"clique_smiths": [
{
"name": "Alice"
},
{
"name": "Bob"
},
{
"name": "Charlie"
}
],
"sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"technical_committee": [
"Alice",
"Bob",
"Charlie"
],
"treasury_funder_pubkey": "FHNpKmJrUtusuvKPGomAygQqeiks98bdV6yD61Stb6vg",
"ud": 1000,
"initial_monetary_mass": 8000,
"current_block": {
"number": 0,
"medianTime": 1700000000
}
}
{
"first_ud": 1000,
"first_ud_reeval": 100,
"first_ud": null,
"first_ud_reeval": null,
"genesis_parameters": {
"genesis_certs_expire_on": 1000,
"genesis_certs_min_received": 2,
......@@ -11,19 +11,43 @@
},
"identities": {
"Alice": {
"index": 1,
"balance": 1000,
"certs": ["Bob", "Charlie"],
"pubkey": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
"certs_received": {
"Bob": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Bob": {
"index": 2,
"balance": 1000,
"certs": ["Alice", "Charlie"],
"pubkey": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
"certs_received": {
"Alice": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Charlie": {
"index": 3,
"balance": 1000,
"certs": ["Alice", "Bob"],
"pubkey": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
"certs_received": {
"Alice": 2700000000,
"Bob": 2700000000
},
"owner_address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
}
},
"parameters": {
......@@ -35,32 +59,38 @@
"idty_confirm_period": 40,
"idty_creation_period": 50,
"membership_period": 1000,
"pending_membership_period": 500,
"ud_creation_period": 10,
"ud_reeval_period": 100,
"smith_cert_period": 15,
"membership_renewal_period": 500,
"ud_creation_period": 60000,
"ud_reeval_period": 600000,
"smith_cert_max_by_issuer": 8,
"smith_cert_min_received_cert_to_issue_cert": 2,
"smith_cert_validity_period": 1000,
"smith_membership_period": 1000,
"smith_pending_membership_period": 500,
"smiths_wot_first_cert_issuable_on": 20,
"smiths_wot_min_cert_for_membership": 2,
"smith_inactivity_max_duration": 48,
"smith_wot_min_cert_for_membership": 2,
"wot_first_cert_issuable_on": 20,
"wot_min_cert_for_create_idty_right": 2,
"wot_min_cert_for_membership": 2
},
"smiths": {
"Alice": {
"certs": ["Bob", "Charlie"]
"clique_smiths": [
{
"name": "Alice"
},
"Bob": {
"certs": ["Alice", "Charlie"]
{
"name": "Bob"
},
"Charlie": {
"certs": ["Alice", "Bob"]
{
"name": "Charlie"
}
},
],
"sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"technical_committee": ["Alice", "Bob", "Charlie"]
"technical_committee": [
"Alice",
"Bob",
"Charlie"
],
"treasury_funder_pubkey": "FHNpKmJrUtusuvKPGomAygQqeiks98bdV6yD61Stb6vg",
"ud": 1000,
"initial_monetary_mass": 3000,
"current_block": {
"number": 0,
"medianTime": 1700000000
}
}
{
"first_ud": 1000,
"first_ud_reeval": 100,
"first_ud": null,
"first_ud_reeval": null,
"genesis_parameters": {
"genesis_certs_min_received": 2,
"genesis_memberships_expire_on": 100000,
......@@ -9,24 +9,81 @@
},
"identities": {
"Alice": {
"index": 1,
"balance": 1000,
"certs": ["Bob", "Charlie"],
"pubkey": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"
"certs_received": {
"Bob": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Bob": {
"index": 2,
"balance": 1000,
"certs": ["Alice", "Charlie"],
"pubkey": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
"certs_received": {
"Alice": 2700000000,
"Charlie": 2700000000
},
"owner_address": "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Charlie": {
"index": 3,
"balance": 1000,
"certs": ["Alice", "Bob"],
"pubkey": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y"
"certs_received": {
"Alice": 2700000000,
"Bob": 2700000000
},
"owner_address": "5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Dave": {
"index": 4,
"balance": 1000,
"certs_received": {
"Alice": 2700000000,
"Bob": 2700000000
},
"owner_address": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
},
"Eve": {
"index": 5,
"balance": 1000,
"certs_received": {
"Alice": 2700000000,
"Bob": 2700000000
},
"owner_address": "5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": true,
"next_cert_issuable_on": 0
},
"Ferdie": {
"index": 6,
"balance": 1000,
"certs": ["Alice", "Bob"],
"pubkey": "5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy"
"certs_received": {
"Alice": 2700000000
},
"owner_address": "5CiPPseXPECbkjWCa6MnjNokrgYjMqmKndv2rSnekmSK2DjL",
"membership_expire_on": 2700000000,
"membership_revokes_on": 2700000001,
"revoked": false,
"next_cert_issuable_on": 0
}
},
"parameters": {
......@@ -38,32 +95,38 @@
"idty_confirm_period": 40,
"idty_creation_period": 50,
"membership_period": 1000,
"pending_membership_period": 500,
"ud_creation_period": 10,
"ud_reeval_period": 100,
"smith_cert_period": 15,
"membership_renewal_period": 500,
"ud_creation_period": 60000,
"ud_reeval_period": 600000,
"smith_cert_max_by_issuer": 8,
"smith_cert_min_received_cert_to_issue_cert": 2,
"smith_cert_validity_period": 1000,
"smith_membership_period": 1000,
"smith_pending_membership_period": 500,
"smiths_wot_first_cert_issuable_on": 20,
"smiths_wot_min_cert_for_membership": 2,
"smith_inactivity_max_duration": 48,
"smith_wot_min_cert_for_membership": 2,
"wot_first_cert_issuable_on": 20,
"wot_min_cert_for_create_idty_right": 2,
"wot_min_cert_for_membership": 2
},
"smiths": {
"Alice": {
"certs": ["Bob", "Charlie"]
"clique_smiths": [
{
"name": "Alice"
},
"Bob": {
"certs": ["Alice", "Charlie"]
{
"name": "Bob"
},
"Charlie": {
"certs": ["Alice", "Bob"]
{
"name": "Charlie"
}
},
],
"sudo_key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"technical_committee": ["Alice", "Bob", "Charlie"]
"technical_committee": [
"Alice",
"Bob",
"Charlie"
],
"treasury_funder_pubkey": "FHNpKmJrUtusuvKPGomAygQqeiks98bdV6yD61Stb6vg",
"ud": 1000,
"initial_monetary_mass": 6000,
"current_block": {
"number": 0,
"medianTime": 1700000000
}
}
\ No newline at end of file
......@@ -14,29 +14,28 @@
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::gdev;
use super::gdev::runtime_types::pallet_balances;
use super::*;
use sp_keyring::AccountKeyring;
use subxt::{ext::sp_runtime::MultiAddress, tx::PairSigner};
use super::{gdev, gdev::runtime_types::pallet_balances, *};
use crate::common::pair_signer::PairSigner;
use sp_keyring::sr25519::Keyring;
use subxt::utils::MultiAddress;
pub async fn set_balance(client: &Client, who: AccountKeyring, amount: u64) -> Result<()> {
pub async fn set_balance(client: &FullClient, who: Keyring, amount: u64) -> Result<()> {
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx()
.sudo()
.sudo(gdev::runtime_types::gdev_runtime::Call::Balances(
pallet_balances::pallet::Call::set_balance {
who: MultiAddress::Id(who.to_account_id()),
.sudo(gdev::runtime_types::gdev_runtime::RuntimeCall::Balances(
pallet_balances::pallet::Call::force_set_balance {
who: MultiAddress::Id(who.to_raw_public().into()),
new_free: amount,
new_reserved: 0,
},
)),
&PairSigner::new(SUDO_ACCOUNT.pair()),
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -45,25 +44,19 @@ pub async fn set_balance(client: &Client, who: AccountKeyring, amount: u64) -> R
Ok(())
}
pub async fn transfer(
client: &Client,
from: AccountKeyring,
amount: u64,
to: AccountKeyring,
) -> Result<()> {
pub async fn transfer(client: &FullClient, from: Keyring, amount: u64, to: Keyring) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let to = MultiAddress::Id(to.to_raw_public().into());
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx()
.universal_dividend()
.transfer_ud(to.clone().into(), amount),
&gdev::tx().universal_dividend().transfer_ud(to, amount),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -72,18 +65,19 @@ pub async fn transfer(
Ok(())
}
pub async fn transfer_all(client: &Client, from: AccountKeyring, to: AccountKeyring) -> Result<()> {
pub async fn transfer_all(client: &FullClient, from: Keyring, to: Keyring) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let to = MultiAddress::Id(to.to_raw_public().into());
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().balances().transfer_all(to.clone().into(), false),
&gdev::tx().balances().transfer_all(to, false),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -93,24 +87,24 @@ pub async fn transfer_all(client: &Client, from: AccountKeyring, to: AccountKeyr
}
pub async fn transfer_ud(
client: &Client,
from: AccountKeyring,
client: &FullClient,
from: Keyring,
amount: u64,
to: AccountKeyring,
to: Keyring,
) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx()
.universal_dividend()
.transfer_ud(to.clone().into(), amount),
.transfer_ud(MultiAddress::Id(to.to_raw_public().into()), amount),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......
......@@ -14,36 +14,44 @@
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::gdev;
use super::gdev::runtime_types::pallet_certification;
use super::*;
use sp_keyring::AccountKeyring;
use subxt::{ext::sp_runtime::MultiAddress, tx::PairSigner};
use super::{gdev, gdev::runtime_types::pallet_certification, *};
use crate::common::pair_signer::PairSigner;
use sp_keyring::sr25519::Keyring;
use subxt::utils::MultiAddress;
pub async fn certify(client: &Client, from: AccountKeyring, to: AccountKeyring) -> Result<()> {
pub async fn certify(client: &FullClient, from: Keyring, to: Keyring) -> Result<()> {
let signer = PairSigner::new(from.pair());
let from = from.to_account_id();
let to = to.to_account_id();
let from: subxt::utils::AccountId32 = from.to_raw_public().into();
let to: subxt::utils::AccountId32 = to.to_raw_public().into();
let issuer_index = client
let _issuer_index = client
.client
.storage()
.fetch(&gdev::storage().identity().identity_index_of(&from), None)
.at_latest()
.await
.unwrap()
.fetch(&gdev::storage().identity().identity_index_of(from.clone()))
.await?
.unwrap();
.unwrap_or_else(|| panic!("{} issuer must exist", from));
let receiver_index = client
.client
.storage()
.fetch(&gdev::storage().identity().identity_index_of(&to), None)
.at_latest()
.await
.unwrap()
.fetch(&gdev::storage().identity().identity_index_of(to))
.await?
.unwrap();
.unwrap_or_else(|| panic!("{} issuer must exist", from));
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().cert().add_cert(issuer_index, receiver_index),
&gdev::tx().certification().add_cert(receiver_index),
&signer,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......
// Copyright 2023 Axiom-Team
//
// This file is part of Duniter-v2S.
//
// Duniter-v2S is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// Duniter-v2S is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::{gdev, gdev::runtime_types::pallet_identity, *};
use crate::{common::pair_signer::PairSigner, DuniterWorld};
use sp_keyring::sr25519::Keyring;
use subxt::{backend::rpc::RpcClient, tx::Signer, utils::AccountId32};
pub async fn request_evaluation(client: &FullClient, origin: Keyring) -> Result<()> {
let origin = PairSigner::new(origin.pair());
let _events = create_block_with_extrinsic(
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().distance().request_distance_evaluation(),
&origin,
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
.await?;
Ok(())
}
pub async fn run_oracle(client: &FullClient, origin: Keyring, rpc_url: String) -> Result<()> {
let origin = PairSigner::new(origin.pair());
let account_id: &AccountId32 = origin.account_id();
if let Some((distances, _current_session, _evaluation_result_path)) =
distance_oracle::compute_distance_evaluation(
&distance_oracle::api::client(rpc_url.clone()).await,
&distance_oracle::Settings {
evaluation_result_dir: PathBuf::default(),
rpc_url,
},
)
.await
{
// Distance evaluation period is 7 blocks
for _ in 0..7 {
super::create_empty_block(&client.rpc).await?;
}
let _events = create_block_with_extrinsic(
&client.rpc,
client.client
.tx()
.create_signed(
&gdev::tx().sudo().sudo(gdev::runtime_types::gdev_runtime::RuntimeCall::Distance(
gdev::runtime_types::pallet_distance::pallet::Call::force_update_evaluation {
evaluator: account_id.clone(),
computation_result:
gdev::runtime_types::sp_distance::ComputationResult {
distances: distances.into_iter().map(|res| unsafe{std::mem::transmute(res)}).collect(),
},
},
)
),
&origin,
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
.await?;
/*for event in events.iter() {
let event = event.unwrap();
println!(
"Event: {}::{} -> {:?}\n\n",
event.pallet_name(),
event.variant_name(),
event.field_values()
);
}*/
}
Ok(())
}
......@@ -14,37 +14,35 @@
// You should have received a copy of the GNU Affero General Public License
// along with Substrate-Libre-Currency. If not, see <https://www.gnu.org/licenses/>.
use super::gdev;
use super::gdev::runtime_types::pallet_identity;
use super::*;
use crate::DuniterWorld;
use sp_keyring::AccountKeyring;
use subxt::tx::PairSigner;
use super::{gdev, gdev::runtime_types::pallet_identity, *};
use crate::{
common::pair_signer::PairSigner, gdev::runtime_types::pallet_identity::types::IdtyName,
DuniterWorld,
};
use sp_keyring::sr25519::Keyring;
use subxt::config::substrate::MultiAddress;
type BlockNumber = u32;
type AccountId = subxt::ext::sp_core::crypto::AccountId32;
type AccountId = subxt::utils::AccountId32;
type IdtyData = gdev::runtime_types::common_runtime::entities::IdtyData;
type IdtyValue =
gdev::runtime_types::pallet_identity::types::IdtyValue<BlockNumber, AccountId, IdtyData>;
// submit extrinsics
pub async fn create_identity(
client: &Client,
from: AccountKeyring,
to: AccountKeyring,
) -> Result<()> {
pub async fn create_identity(client: &FullClient, from: Keyring, to: Keyring) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let to = to.to_raw_public();
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().identity().create_identity(to),
&gdev::tx().identity().create_identity(to.into()),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -53,17 +51,19 @@ pub async fn create_identity(
Ok(())
}
pub async fn confirm_identity(client: &Client, from: AccountKeyring, pseudo: String) -> Result<()> {
pub async fn confirm_identity(client: &FullClient, from: Keyring, pseudo: String) -> Result<()> {
let from = PairSigner::new(from.pair());
let pseudo: IdtyName = IdtyName(pseudo.as_bytes().to_vec());
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().identity().confirm_identity(pseudo),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -72,20 +72,29 @@ pub async fn confirm_identity(client: &Client, from: AccountKeyring, pseudo: Str
Ok(())
}
// get identity value from account keyring name
pub async fn get_identity_value(world: &mut DuniterWorld, account: String) -> Result<IdtyValue> {
let account = AccountKeyring::from_str(&account)
// get identity index from account keyring name
pub async fn get_identity_index(world: &DuniterWorld, account: String) -> Result<u32> {
let account: AccountId = Keyring::from_str(&account)
.expect("unknown account")
.to_account_id();
.to_raw_public()
.into();
let identity_index = world
.read(&gdev::storage().identity().identity_index_of(&account))
.await
.await?
.ok_or_else(|| anyhow::anyhow!("identity {} has no associated index", account))
.unwrap();
Ok(identity_index)
}
// get identity value from account keyring name
pub async fn get_identity_value(world: &DuniterWorld, account: String) -> Result<IdtyValue> {
let identity_index = get_identity_index(world, account).await.unwrap();
let identity_value = world
.read(&gdev::storage().identity().identities(identity_index))
.await
.await?
.ok_or_else(|| {
anyhow::anyhow!(
......
......@@ -18,41 +18,51 @@
pub mod balances;
pub mod cert;
pub mod distance;
pub mod identity;
pub mod oneshot;
#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")]
#[subxt::subxt(
runtime_metadata_path = "../resources/metadata.scale",
derive_for_all_types = "Eq, PartialEq"
)]
pub mod gdev {}
use anyhow::anyhow;
use parity_scale_codec::Encode;
use codec::Encode;
use notify_debouncer_mini::new_debouncer;
use serde_json::Value;
use sp_keyring::AccountKeyring;
use std::io::prelude::*;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use subxt::ext::{sp_core, sp_runtime};
use subxt::rpc::rpc_params;
use subxt::tx::BaseExtrinsicParamsBuilder;
use sp_keyring::sr25519::Keyring;
use std::{
io::prelude::*,
path::{Path, PathBuf},
process::Command,
str::FromStr,
time::{Duration, Instant},
};
use subxt::{
backend::rpc::RpcClient,
config::{substrate::SubstrateExtrinsicParamsBuilder, SubstrateExtrinsicParams},
ext::subxt_rpcs::client::{rpc_params, RpcParams},
};
pub type Client = subxt::OnlineClient<GdevConfig>;
pub type Event = gdev::Event;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
pub type SubmittableExtrinsic = subxt::tx::SubmittableExtrinsic<GdevConfig, Client>;
pub type SubmittableExtrinsic = subxt::tx::SubmittableTransaction<GdevConfig, Client>;
pub type TxProgress = subxt::tx::TxProgress<GdevConfig, Client>;
pub enum GdevConfig {}
impl subxt::config::Config for GdevConfig {
type Index = u32;
type BlockNumber = u32;
type Hash = sp_core::H256;
type Hashing = sp_runtime::traits::BlakeTwo256;
type AccountId = sp_runtime::AccountId32;
type Address = sp_runtime::MultiAddress<Self::AccountId, u32>;
type Header = sp_runtime::generic::Header<Self::BlockNumber, sp_runtime::traits::BlakeTwo256>;
type Signature = sp_runtime::MultiSignature;
type ExtrinsicParams = subxt::tx::BaseExtrinsicParams<Self, Tip>;
type AccountId = subxt::utils::AccountId32;
type Address = subxt::utils::MultiAddress<Self::AccountId, u32>;
type AssetId = ();
type ExtrinsicParams = SubstrateExtrinsicParams<Self>;
type Hash = subxt::utils::H256;
type Hasher = subxt::config::substrate::BlakeTwo256;
type Header =
subxt::config::substrate::SubstrateHeader<u32, subxt::config::substrate::BlakeTwo256>;
type Signature = subxt::utils::MultiSignature;
}
#[derive(Copy, Clone, Debug, Default, Encode)]
......@@ -61,6 +71,11 @@ pub struct Tip {
tip: u64,
}
pub struct FullClient {
pub rpc: RpcClient,
pub client: Client,
}
impl Tip {
pub fn new(amount: u64) -> Self {
Tip { tip: amount }
......@@ -73,7 +88,7 @@ impl From<u64> for Tip {
}
}
pub const SUDO_ACCOUNT: AccountKeyring = AccountKeyring::Alice;
pub const SUDO_ACCOUNT: Keyring = Keyring::Alice;
pub struct Process(std::process::Child);
impl Process {
......@@ -82,16 +97,27 @@ impl Process {
}
}
// Do not let the process keep running after the tests ended
impl Drop for Process {
fn drop(&mut self) {
self.kill()
}
}
pub const DISTANCE_ORACLE_LOCAL_PATH: &str = "../target/debug/distance-oracle";
const DUNITER_DOCKER_PATH: &str = "/usr/local/bin/duniter";
const DUNITER_LOCAL_PATH: &str = "../target/debug/duniter";
struct FullNode {
process: Process,
p2p_port: u16,
ws_port: u16,
rpc_port: u16,
}
pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Client, Process) {
pub async fn spawn_node(
maybe_genesis_conf_file: Option<PathBuf>,
no_spawn: bool,
) -> (FullClient, Option<Process>, u16) {
println!("maybe_genesis_conf_file={:?}", maybe_genesis_conf_file);
let duniter_binary_path = std::env::var("DUNITER_BINARY_PATH").unwrap_or_else(|_| {
if std::path::Path::new(DUNITER_DOCKER_PATH).exists() {
......@@ -101,34 +127,55 @@ pub async fn spawn_node(maybe_genesis_conf_file: Option<PathBuf>) -> (Client, Pr
}
});
let mut the_rpc_port = 9944;
let mut opt_process = None;
// Eventually spawn a node (we most likely will - unless --no-spawn option is used)
if !no_spawn {
let FullNode {
process,
p2p_port: _,
ws_port,
rpc_port,
} = spawn_full_node(
&["--dev", "--execution=Native", "--sealing=manual"],
&[
"--chain=gdev_dev",
"--execution=Native",
"--sealing=manual",
// Necessary options which were previously set by --dev option:
"--force-authoring",
"--rpc-cors=all",
"--alice",
"--tmp",
"--unsafe-force-node-key-generation",
// Fix: End2End test may fail due to network discovery. This option disables automatic peer discovery.π
"--reserved-only",
// prevent local network discovery (even it does not connect due to above flag)
"--no-mdns",
],
&duniter_binary_path,
maybe_genesis_conf_file,
);
let client = Client::from_url(format!("ws://127.0.0.1:{}", ws_port))
opt_process = Some(process);
the_rpc_port = rpc_port;
}
let rpc = RpcClient::from_url(format!("ws://127.0.0.1:{}", the_rpc_port))
.await
.expect("fail to connect to node");
.expect("Failed to create the rpc backend");
let client = Client::from_rpc_client(rpc.clone()).await.unwrap();
(client, process)
(FullClient { rpc, client }, opt_process, the_rpc_port)
}
pub async fn create_empty_block(client: &Client) -> Result<()> {
pub async fn create_empty_block(client: &RpcClient) -> Result<()> {
// Create an empty block
let _: Value = client
.rpc()
.request("engine_createBlock", rpc_params![true, false, Value::Null])
.request("engine_createBlock", rpc_params![true, true, Value::Null])
.await?;
Ok(())
}
pub async fn create_block_with_extrinsic(
client: &Client,
client: &RpcClient,
extrinsic: SubmittableExtrinsic,
) -> Result<subxt::blocks::ExtrinsicEvents<GdevConfig>> {
//println!("extrinsic encoded: {}", hex::encode(extrinsic.encoded()));
......@@ -137,13 +184,12 @@ pub async fn create_block_with_extrinsic(
// Create a non-empty block
let _: Value = client
.rpc()
.request("engine_createBlock", rpc_params![false, false, Value::Null])
.request("engine_createBlock", rpc_params![false, true, Value::Null])
.await?;
// Get extrinsic events
watcher
.wait_for_in_block()
.wait_for_finalized()
.await?
.fetch_events()
.await
......@@ -158,16 +204,16 @@ fn spawn_full_node(
// Ports
let p2p_port = portpicker::pick_unused_port().expect("No ports free");
let rpc_port = portpicker::pick_unused_port().expect("No ports free");
let ws_port = portpicker::pick_unused_port().expect("No ports free");
// Env vars
let mut envs = Vec::new();
if let Some(genesis_conf_file) = maybe_genesis_conf_file {
envs.push(("DUNITER_GENESIS_CONFIG", genesis_conf_file));
envs.push(("DUNITER_GENESIS_CONFIG", genesis_conf_file.clone()));
envs.push(("DUNITER_GENESIS_DATA", genesis_conf_file));
}
// Logs
let log_file_path = format!("duniter-v2s-{}.log", ws_port);
let log_file_path = format!("duniter-v2s-{}.log", rpc_port);
let log_file = std::fs::File::create(&log_file_path).expect("fail to create log file");
// Command
......@@ -177,13 +223,10 @@ fn spawn_full_node(
[
"--no-telemetry",
"--no-prometheus",
"--tmp",
"--port",
&p2p_port.to_string(),
"--rpc-port",
&rpc_port.to_string(),
"--ws-port",
&ws_port.to_string(),
]
.iter()
.chain(args),
......@@ -197,9 +240,9 @@ fn spawn_full_node(
let timeout =
if let Ok(duration_string) = std::env::var("DUNITER_END2END_TESTS_SPAWN_NODE_TIMEOUT") {
duration_string.parse().unwrap_or(4)
duration_string.parse().unwrap_or(10)
} else {
4
10
};
wait_until_log_line(
......@@ -211,22 +254,42 @@ fn spawn_full_node(
FullNode {
process,
p2p_port,
ws_port,
rpc_port,
}
}
fn wait_until_log_line(expected_log_line: &str, log_file_path: &str, timeout: std::time::Duration) {
fn wait_until_log_line(expected_log_line: &str, log_file_path: &str, timeout: Duration) {
if cfg!(target_os = "macos") {
// MacOs seems to not be able to use inotify (buggy)
// So we use a specific implementation for `wait_until_log_line()` here
let start = Instant::now();
loop {
let now = Instant::now();
if now.duration_since(start) > timeout {
eprintln!("Timeout starting node");
std::process::exit(1);
}
if has_log_line(log_file_path, expected_log_line) {
// Ready
return;
}
std::thread::sleep(Duration::from_millis(100));
}
} else {
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = notify::watcher(tx, std::time::Duration::from_millis(100)).unwrap();
use notify::Watcher as _;
watcher
.watch(log_file_path, notify::RecursiveMode::NonRecursive)
let mut debouncer = new_debouncer(std::time::Duration::from_millis(100), tx).unwrap();
debouncer
.watcher()
.watch(
Path::new(log_file_path),
notify::RecursiveMode::NonRecursive,
)
.unwrap();
let mut pos = 0;
loop {
match rx.recv_timeout(timeout) {
Ok(notify::DebouncedEvent::Write(_)) => {
Ok(_) => {
let mut file = std::fs::File::open(log_file_path).unwrap();
file.seek(std::io::SeekFrom::Start(pos)).unwrap();
pos = file.metadata().unwrap().len();
......@@ -238,7 +301,6 @@ fn wait_until_log_line(expected_log_line: &str, log_file_path: &str, timeout: st
}
}
}
Ok(_) => {}
Err(err) => {
eprintln!("Error: {:?}", err);
std::process::exit(1);
......@@ -246,3 +308,85 @@ fn wait_until_log_line(expected_log_line: &str, log_file_path: &str, timeout: st
}
}
}
}
fn has_log_line(log_file_path: &str, expected_log_line: &str) -> bool {
let mut file = std::fs::File::open(log_file_path).unwrap();
file.seek(std::io::SeekFrom::Start(0)).unwrap();
let reader = std::io::BufReader::new(file);
for line in reader.lines() {
if line.expect("fail to read line").contains(expected_log_line) {
return true;
}
}
false
}
pub fn spawn_distance_oracle(distance_oracle_binary_path: &str, duniter_rpc_port: u16) {
Command::new(distance_oracle_binary_path)
.args(
[
"-u",
&format!("ws://127.0.0.1:{duniter_rpc_port}"),
"-d",
"/tmp/duniter-cucumber/chains/gdev/distance",
]
.iter(),
)
.spawn()
.expect("failed to spawn distance oracle")
.wait()
.unwrap();
}
/// A concrete PairSigner implementation which relies on `sr25519::Pair` for signing
/// and where GdevConfig is the runtime configuration.
mod pair_signer {
use super::*;
use sp_core::{sr25519, Pair as _};
use sp_runtime::{
traits::{IdentifyAccount, Verify},
MultiSignature as SpMultiSignature,
};
use subxt::{
config::substrate::{AccountId32, MultiSignature},
tx::Signer,
Config,
};
#[derive(Clone)]
pub struct PairSigner {
account_id: <GdevConfig as Config>::AccountId,
signer: sr25519::Pair,
}
impl PairSigner {
pub fn new(signer: sr25519::Pair) -> Self {
let account_id =
<SpMultiSignature as Verify>::Signer::from(signer.public()).into_account();
Self {
account_id: AccountId32(account_id.into()),
signer,
}
}
pub fn signer(&self) -> &sr25519::Pair {
&self.signer
}
pub fn account_id(&self) -> &AccountId32 {
&self.account_id
}
}
impl Signer<GdevConfig> for PairSigner {
fn account_id(&self) -> <GdevConfig as Config>::AccountId {
self.account_id.clone()
}
fn sign(&self, signer_payload: &[u8]) -> <GdevConfig as Config>::Signature {
let signature = self.signer.sign(signer_payload);
MultiSignature::Sr25519(signature.0)
}
}
}
......@@ -14,19 +14,18 @@
// You should have received a copy of the GNU Affero General Public License
// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
use super::gdev;
use super::gdev::runtime_types::pallet_balances;
use super::gdev::runtime_types::pallet_oneshot_account;
use super::*;
use sp_keyring::AccountKeyring;
use subxt::{
ext::sp_runtime::{AccountId32, MultiAddress},
tx::PairSigner,
use super::{
gdev,
gdev::runtime_types::{pallet_balances, pallet_oneshot_account},
*,
};
use crate::common::pair_signer::PairSigner;
use sp_keyring::sr25519::Keyring;
use subxt::utils::{AccountId32, MultiAddress};
pub enum Account {
Normal(AccountKeyring),
Oneshot(AccountKeyring),
Normal(Keyring),
Oneshot(Keyring),
}
impl Account {
......@@ -34,35 +33,36 @@ impl Account {
&self,
) -> pallet_oneshot_account::types::Account<MultiAddress<AccountId32, ()>> {
match self {
Account::Normal(account) => {
pallet_oneshot_account::types::Account::Normal(account.to_account_id().into())
}
Account::Oneshot(account) => {
pallet_oneshot_account::types::Account::Oneshot(account.to_account_id().into())
}
Account::Normal(account) => pallet_oneshot_account::types::Account::Normal(
MultiAddress::Id(account.to_raw_public().into()),
),
Account::Oneshot(account) => pallet_oneshot_account::types::Account::Oneshot(
MultiAddress::Id(account.to_raw_public().into()),
),
}
}
}
pub async fn create_oneshot_account(
client: &Client,
from: AccountKeyring,
client: &FullClient,
from: Keyring,
amount: u64,
to: AccountKeyring,
to: Keyring,
) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let to = MultiAddress::Id(to.to_raw_public().into());
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx()
.oneshot_account()
.create_oneshot_account(to.into(), amount),
.create_oneshot_account(to, amount),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -72,21 +72,22 @@ pub async fn create_oneshot_account(
}
pub async fn consume_oneshot_account(
client: &Client,
from: AccountKeyring,
client: &FullClient,
from: Keyring,
to: Account,
) -> Result<()> {
let from = PairSigner::new(from.pair());
let to = to.to_account_id();
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx().oneshot_account().consume_oneshot_account(0, to),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......@@ -97,8 +98,8 @@ pub async fn consume_oneshot_account(
#[allow(clippy::too_many_arguments)]
pub async fn consume_oneshot_account_with_remaining(
client: &Client,
from: AccountKeyring,
client: &FullClient,
from: Keyring,
amount: u64,
to: Account,
remaining_to: Account,
......@@ -108,15 +109,16 @@ pub async fn consume_oneshot_account_with_remaining(
let remaining_to = remaining_to.to_account_id();
let _events = create_block_with_extrinsic(
client,
&client.rpc,
client
.client
.tx()
.create_signed(
&gdev::tx()
.oneshot_account()
.consume_oneshot_account_with_remaining(0, to, remaining_to, amount),
&from,
BaseExtrinsicParamsBuilder::new(),
SubstrateExtrinsicParamsBuilder::new().build(),
)
.await?,
)
......
......@@ -16,21 +16,22 @@
mod common;
use async_trait::async_trait;
use common::*;
use cucumber::{given, then, when, World, WorldInit};
use sp_keyring::AccountKeyring;
use std::convert::Infallible;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{
use cucumber::{given, then, when, StatsWriter, World};
use sp_keyring::sr25519::Keyring;
use std::{
path::PathBuf,
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use subxt::backend::rpc::RpcClient;
// ===== world =====
#[derive(WorldInit)]
#[derive(cucumber::World, Default)]
pub struct DuniterWorld {
ignore_errors: bool,
inner: Option<DuniterWorldInner>,
......@@ -38,63 +39,88 @@ pub struct DuniterWorld {
impl DuniterWorld {
// Write methods
async fn init(&mut self, maybe_genesis_conf_file: Option<PathBuf>) {
async fn init(&mut self, maybe_genesis_conf_file: Option<PathBuf>, no_spawn: bool) {
if let Some(ref mut inner) = self.inner {
inner.kill();
}
self.inner = Some(DuniterWorldInner::new(maybe_genesis_conf_file).await);
self.inner = Some(DuniterWorldInner::new(maybe_genesis_conf_file, no_spawn).await);
}
fn kill(&mut self) {
if let Some(ref mut inner) = self.inner {
inner.kill();
}
}
fn set_ignore_errors(&mut self, ignore_errors: bool) {
self.ignore_errors = ignore_errors;
}
// Read methods
fn rpc_client(&self) -> &RpcClient {
if let Some(ref inner) = self.inner {
&inner.client.rpc
} else {
panic!("uninit")
}
}
fn client(&self) -> &Client {
if let Some(ref inner) = self.inner {
&inner.client.client
} else {
panic!("uninit")
}
}
fn full_client(&self) -> &FullClient {
if let Some(ref inner) = self.inner {
&inner.client
} else {
panic!("uninit")
}
}
// Read methods
fn ignore_errors(&self) -> bool {
self.ignore_errors
}
// Read storage entry on last block
fn read<'a, Address>(
async fn read<'a, Address>(
&self,
address: &'a Address,
) -> impl std::future::Future<
Output = std::result::Result<
Option<<Address::Target as subxt::metadata::DecodeWithMetadata>::Target>,
subxt::error::Error,
>,
Output = std::result::Result<Option<Address::Target>, subxt::error::Error>,
> + 'a
where
Address: subxt::storage::StorageAddress<IsFetchable = subxt::storage::address::Yes> + 'a,
Address: subxt::storage::Address<IsFetchable = subxt::custom_values::Yes> + 'a,
{
self.client().storage().fetch(address, None)
self.client()
.storage()
.at_latest()
.await
.unwrap()
.fetch(address)
}
// Read storage entry with default value (on last block)
fn read_or_default<'a, Address>(
async fn read_or_default<'a, Address>(
&self,
address: &'a Address,
) -> impl std::future::Future<
Output = std::result::Result<
<Address::Target as subxt::metadata::DecodeWithMetadata>::Target,
subxt::error::Error,
>,
> + 'a
) -> impl std::future::Future<Output = std::result::Result<Address::Target, subxt::error::Error>> + 'a
where
Address: subxt::storage::StorageAddress<
IsFetchable = subxt::storage::address::Yes,
IsDefaultable = subxt::storage::address::Yes,
Address: subxt::storage::Address<
IsFetchable = subxt::custom_values::Yes,
IsDefaultable = subxt::custom_values::Yes,
> + 'a,
{
self.client().storage().fetch_or_default(address, None)
self.client()
.storage()
.at_latest()
.await
.unwrap()
.fetch_or_default(address)
}
}
......@@ -104,31 +130,26 @@ impl std::fmt::Debug for DuniterWorld {
}
}
#[async_trait(?Send)]
impl World for DuniterWorld {
// We do require some error type.
type Error = Infallible;
async fn new() -> std::result::Result<Self, Infallible> {
Ok(Self {
ignore_errors: false,
inner: None,
})
}
}
struct DuniterWorldInner {
client: Client,
process: Process,
client: FullClient,
process: Option<Process>,
ws_port: u16,
}
impl DuniterWorldInner {
async fn new(maybe_genesis_conf_file: Option<PathBuf>) -> Self {
let (client, process) = spawn_node(maybe_genesis_conf_file).await;
DuniterWorldInner { client, process }
async fn new(maybe_genesis_conf_file: Option<PathBuf>, no_spawn: bool) -> Self {
let (client, process, ws_port) = spawn_node(maybe_genesis_conf_file, no_spawn).await;
DuniterWorldInner {
client,
process,
ws_port,
}
}
fn kill(&mut self) {
self.process.kill();
if let Some(p) = &mut self.process {
p.kill();
}
}
}
......@@ -144,36 +165,40 @@ fn parse_amount(amount: u64, unit: &str) -> (u64, bool) {
// ===== given =====
#[allow(clippy::needless_pass_by_ref_mut)]
#[given(regex = r"([a-zA-Z]+) ha(?:ve|s) (\d+) (ĞD|cĞD|UD|mUD)")]
async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: String) -> Result<()> {
// Parse inputs
let who = AccountKeyring::from_str(&who).expect("unknown to");
let who = Keyring::from_str(&who).expect("unknown to");
let (mut amount, is_ud) = parse_amount(amount, &unit);
if is_ud {
let current_ud_amount = world
.read(&gdev::storage().universal_dividend().current_ud())
.await
.await?
.unwrap_or_default();
amount = (amount * current_ud_amount) / 1_000;
}
// Create {amount} ĞD for {who}
common::balances::set_balance(world.client(), who, amount).await?;
common::balances::set_balance(world.full_client(), who, amount).await?;
Ok(())
}
// ===== when =====
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"(\d+) blocks? later")]
async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> {
for _ in 0..n {
common::create_empty_block(world.client()).await?;
common::create_empty_block(world.rpc_client()).await?;
}
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ĞD|cĞD|UD|mUD) to ([a-zA-Z]+)$")]
async fn transfer(
world: &mut DuniterWorld,
......@@ -183,14 +208,14 @@ async fn transfer(
to: String,
) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
let (amount, is_ud) = parse_amount(amount, &unit);
let res = if is_ud {
common::balances::transfer_ud(world.client(), from, amount, to).await
common::balances::transfer_ud(world.full_client(), from, amount, to).await
} else {
common::balances::transfer(world.client(), from, amount, to).await
common::balances::transfer(world.full_client(), from, amount, to).await
};
if world.ignore_errors() {
......@@ -200,6 +225,7 @@ async fn transfer(
}
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"([a-zA-Z]+) sends? (\d+) (ĞD|cĞD) to oneshot ([a-zA-Z]+)")]
async fn create_oneshot_account(
world: &mut DuniterWorld,
......@@ -209,15 +235,16 @@ async fn create_oneshot_account(
to: String,
) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
let (amount, is_ud) = parse_amount(amount, &unit);
assert!(!is_ud);
common::oneshot::create_oneshot_account(world.client(), from, amount, to).await
common::oneshot::create_oneshot_account(world.full_client(), from, amount, to).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"oneshot ([a-zA-Z]+) consumes? into (oneshot|account) ([a-zA-Z]+)")]
async fn consume_oneshot_account(
world: &mut DuniterWorld,
......@@ -226,21 +253,22 @@ async fn consume_oneshot_account(
to: String,
) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
let to = match is_dest_oneshot.as_str() {
"oneshot" => common::oneshot::Account::Oneshot(to),
"account" => common::oneshot::Account::Normal(to),
_ => unreachable!(),
};
common::oneshot::consume_oneshot_account(world.client(), from, to).await
common::oneshot::consume_oneshot_account(world.full_client(), from, to).await
}
#[when(
regex = r"oneshot ([a-zA-Z]+) consumes? (\d+) (ĞD|cĞD) into (oneshot|account) ([a-zA-Z]+) and the rest into (oneshot|account) ([a-zA-Z]+)"
)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::needless_pass_by_ref_mut)]
async fn consume_oneshot_account_with_remaining(
world: &mut DuniterWorld,
from: String,
......@@ -252,9 +280,9 @@ async fn consume_oneshot_account_with_remaining(
remaining_to: String,
) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let remaining_to = AccountKeyring::from_str(&remaining_to).expect("unknown remaining_to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
let remaining_to = Keyring::from_str(&remaining_to).expect("unknown remaining_to");
let to = match is_dest_oneshot.as_str() {
"oneshot" => common::oneshot::Account::Oneshot(to),
"account" => common::oneshot::Account::Normal(to),
......@@ -270,7 +298,7 @@ async fn consume_oneshot_account_with_remaining(
assert!(!is_ud);
common::oneshot::consume_oneshot_account_with_remaining(
world.client(),
world.full_client(),
from,
amount,
to,
......@@ -279,62 +307,116 @@ async fn consume_oneshot_account_with_remaining(
.await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"([a-zA-Z]+) sends? all (?:his|her) (?:ĞDs?|DUs?|UDs?) to ([a-zA-Z]+)")]
async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
common::balances::transfer_all(world.client(), from, to).await
common::balances::transfer_all(world.full_client(), from, to).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"([a-zA-Z]+) certifies ([a-zA-Z]+)")]
async fn certifies(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
common::cert::certify(world.client(), from, to).await
common::cert::certify(world.full_client(), from, to).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r"([a-zA-Z]+) creates identity for ([a-zA-Z]+)")]
async fn creates_identity(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
// Parse inputs
let from = AccountKeyring::from_str(&from).expect("unknown from");
let to = AccountKeyring::from_str(&to).expect("unknown to");
let from = Keyring::from_str(&from).expect("unknown from");
let to = Keyring::from_str(&to).expect("unknown to");
common::identity::create_identity(world.client(), from, to).await
common::identity::create_identity(world.full_client(), from, to).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r#"([a-zA-Z]+) confirms (?:his|her) identity with pseudo "([a-zA-Z]+)""#)]
async fn confirm_identity(world: &mut DuniterWorld, from: String, pseudo: String) -> Result<()> {
let from = AccountKeyring::from_str(&from).expect("unknown from");
let from = Keyring::from_str(&from).expect("unknown from");
common::identity::confirm_identity(world.client(), from, pseudo).await
common::identity::confirm_identity(world.full_client(), from, pseudo).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r#"([a-zA-Z]+) requests distance evaluation"#)]
async fn request_distance_evaluation(world: &mut DuniterWorld, who: String) -> Result<()> {
let who = Keyring::from_str(&who).expect("unknown origin");
common::distance::request_evaluation(world.full_client(), who).await
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[when(regex = r#"([a-zA-Z]+) runs distance oracle"#)]
async fn run_distance_oracle(world: &mut DuniterWorld, who: String) -> Result<()> {
let who = Keyring::from_str(&who).expect("unknown origin");
common::distance::run_oracle(
world.full_client(),
who,
format!("ws://127.0.0.1:{}", world.inner.as_ref().unwrap().ws_port),
)
.await
}
// ===== then ====
#[then(regex = r"([a-zA-Z]+) should have (\d+) (ĞD|cĞD)")]
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"treasury should contain (\d+) (ĞD|cĞD)")]
async fn treasury_should_contain(
world: &mut DuniterWorld,
amount: u64,
unit: String,
) -> Result<()> {
let who =
subxt::utils::AccountId32::from_str("5EYCAe5ijiYfyeZ2JJCGq56LmPyNRAKzpG4QkoQkkQNB5e6Z")
.expect("invalid treasury account id");
let (amount, _is_ud) = parse_amount(amount, &unit);
let who_account = world
.read_or_default(&gdev::storage().system().account(&who))
.await
.await?;
assert_eq!(who_account.data.free, amount);
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"([a-zA-Z]+) should have (\d+) (ĞD|cĞD)( reserved)?")]
async fn should_have(
world: &mut DuniterWorld,
who: String,
amount: u64,
unit: String,
reserved: String,
) -> Result<()> {
// Parse inputs
let who = AccountKeyring::from_str(&who)
let who: subxt::utils::AccountId32 = Keyring::from_str(&who)
.expect("unknown to")
.to_account_id();
.to_raw_public()
.into();
let (amount, _is_ud) = parse_amount(amount, &unit);
let who_account = world
.read_or_default(&gdev::storage().system().account(&who))
.await
.await?;
if reserved.is_empty() {
assert_eq!(who_account.data.free, amount);
} else {
assert_eq!(who_account.data.reserved, amount);
}
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"([a-zA-Z]+) should have oneshot (\d+) (ĞD|cĞD)")]
async fn should_have_oneshot(
world: &mut DuniterWorld,
......@@ -343,18 +425,21 @@ async fn should_have_oneshot(
unit: String,
) -> Result<()> {
// Parse inputs
let who = AccountKeyring::from_str(&who)
let who: subxt::utils::AccountId32 = Keyring::from_str(&who)
.expect("unknown to")
.to_account_id();
.to_raw_public()
.into();
let (amount, _is_ud) = parse_amount(amount, &unit);
let oneshot_amount = world
.read(&gdev::storage().oneshot_account().oneshot_accounts(&who))
.await
.await?;
assert_eq!(oneshot_amount.unwrap_or(0), amount);
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"Current UD amount should be (\d+).(\d+)")]
async fn current_ud_amount_should_be(
world: &mut DuniterWorld,
......@@ -364,21 +449,25 @@ async fn current_ud_amount_should_be(
let expected = (amount * 100) + cents;
let actual = world
.read_or_default(&gdev::storage().universal_dividend().current_ud())
.await
.await?;
assert_eq!(actual, expected);
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"Monetary mass should be (\d+).(\d+)")]
async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u64) -> Result<()> {
let expected = (amount * 100) + cents;
let actual = world
.read_or_default(&gdev::storage().universal_dividend().monetary_mass())
.await
.await?;
assert_eq!(actual, expected);
Ok(())
}
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"([a-zA-Z]+) should be certified by ([a-zA-Z]+)")]
async fn should_be_certified_by(
world: &mut DuniterWorld,
......@@ -386,12 +475,14 @@ async fn should_be_certified_by(
issuer: String,
) -> Result<()> {
// Parse inputs
let receiver_account = AccountKeyring::from_str(&receiver)
let receiver_account: subxt::utils::AccountId32 = Keyring::from_str(&receiver)
.expect("unknown to")
.to_account_id();
let issuer_account = AccountKeyring::from_str(&issuer)
.to_raw_public()
.into();
let issuer_account: subxt::utils::AccountId32 = Keyring::from_str(&issuer)
.expect("unknown to")
.to_account_id();
.to_raw_public()
.into();
// get corresponding identities index
let issuer_index = world
......@@ -400,6 +491,7 @@ async fn should_be_certified_by(
.identity()
.identity_index_of(&issuer_account),
)
.await
.await?
.unwrap();
let receiver_index = world
......@@ -408,18 +500,24 @@ async fn should_be_certified_by(
.identity()
.identity_index_of(&receiver_account),
)
.await
.await?
.unwrap();
let issuers = world
.read_or_default(&gdev::storage().cert().certs_by_receiver(receiver_index))
.read_or_default(
&gdev::storage()
.certification()
.certs_by_receiver(receiver_index),
)
.await
.await?;
// look for certification by issuer/receiver pair
match issuers.binary_search_by(|(issuer_, _)| issuer_index.cmp(issuer_)) {
match issuers.binary_search_by(|(issuer_, _)| issuer_.cmp(&issuer_index)) {
Ok(_) => Ok(()),
Err(_) => Err(anyhow::anyhow!(
"no certification found from {} to {}: {:?}",
"no certification found from {} ({issuer_index}) to {} ({receiver_index}): {:?}",
issuer,
receiver,
issuers
......@@ -428,30 +526,55 @@ async fn should_be_certified_by(
}
}
#[then(regex = r"([a-zA-Z]+) should (not )?be eligible to UD")]
async fn should_be_eligible_to_ud(
world: &mut DuniterWorld,
identity: String,
not: String,
) -> Result<()> {
let eligible = not.is_empty();
assert_eq!(
identity::get_identity_value(world, identity)
.await
.expect("Identity not found")
.data
.first_eligible_ud
!= 0,
eligible
);
Ok(())
}
use gdev::runtime_types::pallet_identity::types::IdtyStatus;
#[then(regex = r"([a-zA-Z]+) identity should be created")]
async fn identity_should_be_created(world: &mut DuniterWorld, receiver: String) -> Result<()> {
let identity_value = common::identity::get_identity_value(world, receiver).await?;
// status from string
impl FromStr for IdtyStatus {
type Err = String;
match identity_value.status {
IdtyStatus::Created => Ok(()),
IdtyStatus::ConfirmedByOwner | IdtyStatus::Validated => {
Err(anyhow::anyhow!("status not created").into())
fn from_str(input: &str) -> std::result::Result<IdtyStatus, String> {
match input {
"unconfirmed" => Ok(IdtyStatus::Unconfirmed),
"unvalidated" => Ok(IdtyStatus::Unvalidated),
"member" => Ok(IdtyStatus::Member),
"notmember" => Ok(IdtyStatus::NotMember),
"revoked" => Ok(IdtyStatus::Revoked),
_ => Err(format!("'{input}' does not match a status")),
}
}
}
#[then(regex = r"([a-zA-Z]+) identity should be confirmed")]
async fn identity_should_be_confirmed(world: &mut DuniterWorld, name: String) -> Result<()> {
#[allow(clippy::needless_pass_by_ref_mut)]
#[then(regex = r"([a-zA-Z]+) identity should be ([a-zA-Z ]+)")]
async fn identity_status_should_be(
world: &mut DuniterWorld,
name: String,
status: String,
) -> Result<()> {
let identity_value = common::identity::get_identity_value(world, name).await?;
match identity_value.status {
IdtyStatus::ConfirmedByOwner => Ok(()),
IdtyStatus::Created | IdtyStatus::Validated => {
Err(anyhow::anyhow!("status not confirmed by owner").into())
}
}
let expected_status = IdtyStatus::from_str(&status)?;
assert_eq!(identity_value.status, expected_status);
Ok(())
}
// ============================================================
......@@ -459,8 +582,20 @@ async fn identity_should_be_confirmed(world: &mut DuniterWorld, name: String) ->
#[derive(clap::Args)]
struct CustomOpts {
/// Keep running
#[clap(short, long)]
#[arg(short, long)]
keep_running: bool,
/// Do not spawn a node, reuse expected node on port 9944
#[arg(long)]
no_spawn: bool,
/// For compliance with Jetbrains IDE which pushes extra args.
/// https://youtrack.jetbrains.com/issue/CPP-33071/cargo-test-adds-extra-options-which-conflict-with-Cucumber
#[arg(short, long)]
format: Option<String>,
#[arg(short, long = "show-output")]
show_output: bool,
#[arg(short = 'Z', long)]
z: Option<String>,
}
const DOCKER_FEATURES_PATH: &str = "/var/lib/duniter/cucumber-features";
......@@ -480,6 +615,7 @@ async fn main() {
let opts = cucumber::cli::Opts::<_, _, _, CustomOpts>::parsed();
let keep_running = opts.custom.keep_running;
let no_spawn = opts.custom.no_spawn;
// Handle crtl+C
let running = Arc::new(AtomicBool::new(true));
......@@ -489,32 +625,45 @@ async fn main() {
})
.expect("Error setting Ctrl-C handler");
DuniterWorld::cucumber()
//.fail_on_skipped()
let summarize = DuniterWorld::cucumber()
.fail_on_skipped()
.max_concurrent_scenarios(4)
.before(|feature, _rule, scenario, world| {
.before(move |feature, _rule, scenario, world| {
let mut genesis_conf_file_path = PathBuf::new();
genesis_conf_file_path.push("cucumber-genesis");
genesis_conf_file_path.push(&format!(
genesis_conf_file_path.push(format!(
"{}.json",
genesis_conf_name(&feature.tags, &scenario.tags)
));
world.set_ignore_errors(ignore_errors(&scenario.tags));
Box::pin(world.init(Some(genesis_conf_file_path)))
Box::pin(world.init(Some(genesis_conf_file_path), no_spawn))
})
.after(move |_feature, _rule, _scenario, maybe_world| {
.after(move |_feature, _rule, _scenario, _ev, maybe_world| {
if keep_running {
while running.load(Ordering::SeqCst) {}
}
// Early kill (not waiting destructor) to save CPU/memory
if let Some(world) = maybe_world {
world.kill();
}
Box::pin(std::future::ready(()))
})
.with_cli(opts)
.run_and_exit(features_path)
.run(features_path)
.await;
if summarize.failed_steps() > 0 {
panic!("Could not run tests correctly (failed steps)");
}
if summarize.hook_errors() > 0 {
panic!("Could not run tests correctly (hook errors)");
}
if summarize.parsing_errors() > 0 {
panic!("Could not run tests correctly (parsing errors)");
}
if summarize.execution_has_failed() {
panic!("Could not run tests correctly (execution has failed)");
}
}
fn genesis_conf_name(feature_tags: &[String], scenario_tags: &[String]) -> String {
......
[package]
authors = ['Axiom-Team Developers <https://axiom-team.fr>']
description = 'duniter live tests.'
edition = "2021"
homepage = 'https://duniter.org'
license = 'AGPL-3.0'
name = 'duniter-live-tests'
repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
version = '3.0.0'
authors.workspace = true
description = "duniter live tests"
edition.workspace = true
homepage.workspace = true
license.workspace = true
name = "duniter-live-tests"
repository.workspace = true
version.workspace = true
[dev-dependencies]
anyhow = "1.0"
hex-literal = "0.3"
parity-scale-codec = "3.1.5"
sp-core = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.32' }
subxt = { git = 'https://github.com/duniter/subxt', branch = 'duniter-substrate-v0.9.32' }
tokio = { version = "1.15.0", features = ["macros"] }
anyhow = { workspace = true }
codec = { workspace = true }
countmap = { workspace = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
subxt = { workspace = true, features = [
"native",
"jsonrpsee",
] }
tokio = { workspace = true, features = ["macros", "time", "rt-multi-thread"] }
[features]
runtime-benchmarks = []
std = []
try-runtime = []
......@@ -16,7 +16,7 @@ Test suite that verifies the consistency of the onchain storage.
#### Custom RPC endpoint
You can choose to use another RPC endpoint by setting the environment variable `WS_RPC_ENDPOINT`.
This is also the only way to test against a different network that the default one.
This is also the only way to test against a different network that the default one which is `ws://localhost:9944`.
#### run against a specific block
......
......@@ -17,16 +17,17 @@
#[subxt::subxt(runtime_metadata_path = "../resources/metadata.scale")]
pub mod gdev {}
use hex_literal::hex;
use sp_core::crypto::AccountId32;
use sp_core::{blake2_128, ByteArray, H256};
use std::collections::HashMap;
use subxt::config::SubstrateConfig as GdevConfig;
use countmap::CountMap;
use sp_core::{blake2_128, crypto::AccountId32, ByteArray, H256};
use std::collections::{HashMap, HashSet};
use subxt::{backend::rpc::RpcClient, config::SubstrateConfig as GdevConfig};
const DEFAULT_ENDPOINT: &str = "wss://gdev.librelois.fr:443/ws";
const DEFAULT_ENDPOINT: &str = "ws://localhost:9944";
const TREASURY_ACCOUNT_ID: [u8; 32] =
hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000");
const EXISTENTIAL_DEPOSIT: u64 = 100;
//use hex_literal::hex;
//const TREASURY_ACCOUNT_ID: [u8; 32] =
// hex!("6d6f646c70792f74727372790000000000000000000000000000000000000000");
type Client = subxt::OnlineClient<GdevConfig>;
......@@ -38,32 +39,38 @@ type Index = u32;
// Define gdev types
type AccountInfo = gdev::runtime_types::frame_system::AccountInfo<
Index,
gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance>,
gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance, IdtyIndex>,
>;
type IdtyData = gdev::runtime_types::common_runtime::entities::IdtyData;
type IdtyIndex = u32;
type IdtyValue =
gdev::runtime_types::pallet_identity::types::IdtyValue<BlockNumber, AccountId32, IdtyData>;
use gdev::runtime_types::pallet_identity::types::IdtyStatus;
type MembershipData = gdev::runtime_types::sp_membership::MembershipData<BlockNumber>;
use gdev::runtime_types::pallet_identity::types::{IdtyName, IdtyStatus};
struct Storage {
accounts: HashMap<AccountId32, AccountInfo>,
identities: HashMap<IdtyIndex, IdtyValue>,
identity_index_of: HashMap<[u8; 16], IdtyIndex>,
memberships: HashMap<IdtyIndex, MembershipData>,
identities_names: HashMap<IdtyIndex, IdtyName>,
}
#[tokio::test(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
let ws_rpc_endpoint =
std::env::var("WS_RPC_ENDPOINT").unwrap_or_else(|_| DEFAULT_ENDPOINT.to_owned());
let client = Client::from_url(ws_rpc_endpoint)
let rpc = RpcClient::from_url(ws_rpc_endpoint)
.await
.expect("fail to connect to node");
.expect("Failed to create the rpc backend");
let client = Client::from_rpc_client(rpc.clone()).await.unwrap();
let maybe_block_hash = if let Ok(block_number) = std::env::var("AT_BLOCK_NUMBER") {
let block_number: BlockNumber = block_number.parse()?;
println!("Run sanity tests against ĞDev at block #{}.", block_number);
client.rpc().block_hash(Some(block_number.into())).await?
// FIXME
// client.at(block_number).await?
None
} else {
println!("Run sanity tests against ĞDev at last best block");
None
......@@ -72,64 +79,105 @@ async fn main() -> anyhow::Result<()> {
sanity_tests_at(client, maybe_block_hash).await
}
async fn sanity_tests_at(client: Client, maybe_block_hash: Option<H256>) -> anyhow::Result<()> {
async fn sanity_tests_at(client: Client, _maybe_block_hash: Option<H256>) -> anyhow::Result<()> {
// ===== Collect storage ===== //
// Collect accounts
let mut accounts = HashMap::new();
let mut accounts: HashMap<AccountId32, AccountInfo> = HashMap::new();
let mut account_iter = client
.storage()
.iter(
gdev::storage().system().account_root(),
100,
maybe_block_hash,
)
.at_latest()
.await
.unwrap()
.iter(gdev::storage().system().account_iter())
.await?;
while let Some((key, account_info)) = account_iter.next().await? {
while let Some(Ok(key)) = account_iter.next().await {
let mut account_id_bytes = [0u8; 32];
account_id_bytes.copy_from_slice(&key.0[48..]);
accounts.insert(AccountId32::new(account_id_bytes), account_info);
account_id_bytes.copy_from_slice(&key.key_bytes[48..]);
accounts.insert(AccountId32::new(account_id_bytes), key.value);
}
println!("accounts: {}.", accounts.len());
println!("accounts.len(): {}.", accounts.len());
// Collect identities
let mut identities = HashMap::new();
let mut identities: HashMap<IdtyIndex, IdtyValue> = HashMap::new();
let mut idty_iter = client
.storage()
.iter(
gdev::storage().identity().identities_root(),
100,
maybe_block_hash,
)
.at_latest()
.await
.unwrap()
.iter(gdev::storage().identity().identities_iter())
.await?;
while let Some((key, idty_value)) = idty_iter.next().await? {
while let Some(Ok(key)) = idty_iter.next().await {
let mut idty_index_bytes = [0u8; 4];
idty_index_bytes.copy_from_slice(&key.0[40..]);
identities.insert(IdtyIndex::from_le_bytes(idty_index_bytes), idty_value);
idty_index_bytes.copy_from_slice(&key.key_bytes[40..]);
let idty_val = IdtyValue {
data: key.value.data,
next_creatable_identity_on: key.value.next_creatable_identity_on,
old_owner_key: None, // Not used in the live test, skip the conversion
owner_key: AccountId32::from(key.value.owner_key.0),
next_scheduled: key.value.next_scheduled,
status: key.value.status,
};
identities.insert(IdtyIndex::from_le_bytes(idty_index_bytes), idty_val);
}
println!("identities: {}.", identities.len());
println!("identities.len(): {}.", identities.len());
// Collect identity_index_of
let mut identity_index_of = HashMap::new();
let mut identity_index_of: HashMap<[u8; 16], IdtyIndex> = HashMap::new();
let mut idty_index_of_iter = client
.storage()
.iter(
gdev::storage().identity().identity_index_of_root(),
100,
maybe_block_hash,
)
.at_latest()
.await
.unwrap()
.iter(gdev::storage().identity().identity_index_of_iter())
.await?;
while let Some((key, idty_index)) = idty_index_of_iter.next().await? {
while let Some(Ok(key)) = idty_index_of_iter.next().await {
let mut blake2_128_bytes = [0u8; 16];
blake2_128_bytes.copy_from_slice(&key.0[32..]);
identity_index_of.insert(blake2_128_bytes, idty_index);
blake2_128_bytes.copy_from_slice(&key.key_bytes[32..48]);
identity_index_of.insert(blake2_128_bytes, key.value);
}
println!("identity_index_of.len(): {}.", identity_index_of.len());
// Collect identity_names
let mut identities_names: HashMap<IdtyIndex, IdtyName> = HashMap::new();
let mut idty_name_iter = client
.storage()
.at_latest()
.await
.unwrap()
.iter(gdev::storage().identity().identities_names_iter())
.await?;
while let Some(Ok(key)) = idty_name_iter.next().await {
let name = IdtyName(key.key_bytes);
identities_names.insert(key.value, name);
}
println!("identities_names.len(): {}.", identities_names.len());
// Collect memberships
let mut memberships: HashMap<IdtyIndex, MembershipData> = HashMap::new();
let mut membership_iter = client
.storage()
.at_latest()
.await
.unwrap()
.iter(gdev::storage().membership().membership_iter())
.await?;
while let Some(Ok(key)) = membership_iter.next().await {
let mut idty_index_bytes = [0u8; 4];
idty_index_bytes.copy_from_slice(&key.key_bytes[40..]);
let membership_val = MembershipData {
expire_on: key.value.expire_on,
};
memberships.insert(IdtyIndex::from_le_bytes(idty_index_bytes), membership_val);
}
println!("identity_index_of: {}.", identities.len());
println!("memberships.len(): {}.", memberships.len());
let storage = Storage {
accounts,
identities,
identity_index_of,
memberships,
identities_names,
};
// ===== Verify storage ===== //
......@@ -149,12 +197,21 @@ mod verifier {
Self { errors: Vec::new() }
}
/// method to run all storage tests
pub(super) async fn verify_storage(&mut self, storage: &Storage) -> anyhow::Result<()> {
self.verify_accounts(&storage.accounts).await;
self.verify_identities(&storage.accounts, &storage.identities)
.await;
self.verify_identity_index_of(&storage.identities, &storage.identity_index_of)
.await;
self.verify_identity_coherence(&storage.identities, &storage.identity_index_of)
.await;
self.verify_status_coherence(
&storage.identities,
&storage.memberships,
&storage.identities_names,
)
.await;
if self.errors.is_empty() {
Ok(())
......@@ -169,12 +226,19 @@ mod verifier {
}
}
/// assert method to collect errors
fn assert(&mut self, assertion: bool, error: String) {
if !assertion {
self.errors.push(error);
}
}
/// like assert but just push error
fn error(&mut self, error: String) {
self.errors.push(error);
}
/// check accounts sufficients and consumers (specific to duniter-account pallet)
async fn verify_accounts(&mut self, accounts: &HashMap<AccountId32, AccountInfo>) {
for (account_id, account_info) in accounts {
if account_info.sufficients == 0 {
......@@ -185,7 +249,8 @@ mod verifier {
);
// Rule 2: If the account is not sufficient, it should comply to the existential deposit
self.assert(
(account_info.data.free + account_info.data.reserved) >= 200,
(account_info.data.free + account_info.data.reserved)
>= EXISTENTIAL_DEPOSIT,
format!(
"Account {} not respect existential deposit rule.",
account_id
......@@ -193,32 +258,42 @@ mod verifier {
);
}
// Rule 3: If the account have consumers, it shoul have at least one provider
// Rule 3: If the account have consumers, it should have at least one provider
if account_info.consumers > 0 {
// Rule 1: If the account is not s
// Rule 1: If the account is not sufficient [...]
self.assert(
account_info.providers > 0,
format!("Account {} has no providers nor sufficients.", account_id),
);
}
if account_id.as_slice() != TREASURY_ACCOUNT_ID {
// Rule 4: If the account is not a "special account",
// it should have a random id or a consumer
self.assert(
account_info.data.random_id.is_some() || account_info.consumers > 0,
format!("Account {} has no random_id nor consumer.", account_id),
);
}
}
}
/// check list of identities (account existence, sufficient)
async fn verify_identities(
&mut self,
accounts: &HashMap<AccountId32, AccountInfo>,
identities: &HashMap<IdtyIndex, IdtyValue>,
) {
// counts occurence of owner key
let mut countmap = CountMap::<AccountId32, u8>::new();
// list owner key with multiple occurences
let mut duplicates = HashSet::new();
for (idty_index, idty_value) in identities {
countmap.insert_or_increment(idty_value.owner_key.clone());
if let Some(count) = countmap.get_count(&idty_value.owner_key) {
if count > 1 {
self.error(format!(
"address {} is the owner_key of {count} identities",
idty_value.owner_key
));
if count == 2 {
duplicates.insert(idty_value.owner_key.clone());
}
}
}
// Rule 1: each identity should have an account
let maybe_account = accounts.get(&idty_value.owner_key);
self.assert(
......@@ -236,30 +311,19 @@ mod verifier {
),
);
}
match idty_value.status {
IdtyStatus::Validated => {
// Rule 3: If the identity is validated, removable_on shoud be zero
self.assert(
idty_value.removable_on == 0,
format!(
"Identity {} is corrupted: removable_on > 0 on validated idty",
idty_index
),
);
}
_ => {
// Rule 4: If the identity is not validated, next_creatable_identity_on shoud be zero
self.assert(
idty_value.next_creatable_identity_on == 0,
format!("Identity {} is corrupted: next_creatable_identity_on > 0 on non-validated idty",
idty_index)
);
}
for (idty_index, idty_value) in identities {
if duplicates.contains(&idty_value.owner_key) {
self.error(format!(
"duplicate key {} at position {idty_index}",
idty_value.owner_key
));
}
}
}
/// check the identity hashmap (length, identity existence, hash matches owner key)
async fn verify_identity_index_of(
&mut self,
identities: &HashMap<IdtyIndex, IdtyValue>,
......@@ -268,7 +332,11 @@ mod verifier {
// Rule1: identity_index_of should have the same lenght as identities
self.assert(
identities.len() == identity_index_of.len(),
"identities.len() != identity_index_of.len().".to_owned(),
format!(
"identities.len({}) != identity_index_of.len({}).",
identities.len(),
identity_index_of.len()
),
);
for (blake2_128_owner_key, idty_index) in identity_index_of {
......@@ -296,5 +364,139 @@ mod verifier {
}
}
}
/// check identities status and membership coherence
async fn verify_status_coherence(
&mut self,
identities: &HashMap<IdtyIndex, IdtyValue>,
memberships: &HashMap<IdtyIndex, MembershipData>,
names: &HashMap<IdtyIndex, IdtyName>,
) {
for (idty_index, idty_value) in identities {
// Rule 1: each Status::Member
// should have a membership and a name
// membership should be set to expire
// identity should have no scheduled action
if let IdtyStatus::Member = idty_value.status {
self.assert(
memberships.get(idty_index).is_some(),
format!("identity number {idty_index} should have a valid membership"),
);
self.assert(
names.get(idty_index).is_some(),
format!("identity number {idty_index} should have a name"),
);
self.assert(
memberships.get(idty_index).unwrap().expire_on != 0,
format!(
"Member identity number {idty_index} should have a non-null expire_on value"
),
);
self.assert(
identities.get(idty_index).unwrap().next_scheduled == 0,
format!(
"Member identity number {idty_index} should have a null next_scheduled value"
),
);
}
// Rule 2: each Status::NotMember
// should have a name but no membership
// should have a scheduled action (auto-revocation)
if let IdtyStatus::NotMember = idty_value.status {
self.assert(
memberships.get(idty_index).is_none(),
format!("identity number {idty_index} should not have a valid membership"),
);
self.assert(
names.get(idty_index).is_some(),
format!("identity number {idty_index} should have a name"),
);
self.assert(
identities.get(idty_index).unwrap().next_scheduled != 0,
format!("NotMember identity number {idty_index} should have a non-null next_scheduled value"),
);
}
// Rule 3: each Status::Revoked
// should should have a name
// no membership
// should be scheduled for removal
if let IdtyStatus::Revoked = idty_value.status {
self.assert(
memberships.get(idty_index).is_none(),
format!("identity number {idty_index} should not have a valid membership"),
);
self.assert(
names.get(idty_index).is_some(),
format!("identity number {idty_index} should have a name"),
);
self.assert(
identities.get(idty_index).unwrap().next_scheduled != 0,
format!("Revoked identity number {idty_index} should have a non-null next_scheduled value"),
);
}
// Rule 4: each Status::Unvalidaded
// should have a name but no membership.
// should be scheduled for removal
if let IdtyStatus::Unvalidated = idty_value.status {
self.assert(
memberships.get(idty_index).is_none(),
format!("identity number {idty_index} should not have a valid membership"),
);
self.assert(
names.get(idty_index).is_some(),
format!("identity number {idty_index} should have a name"),
);
self.assert(
identities.get(idty_index).unwrap().next_scheduled != 0,
format!("Unvalidated identity number {idty_index} should have a non-null next_scheduled value"),
);
}
// Rule 5: each Status::Unconfirmed
// should not have a name neither a membership.
// should be scheduled for removal soon
if let IdtyStatus::Unconfirmed = idty_value.status {
self.assert(
memberships.get(idty_index).is_none(),
format!("identity number {idty_index} should not have a valid membership"),
);
self.assert(
names.get(idty_index).is_none(),
format!("identity number {idty_index} should not have a name"),
);
self.assert(
identities.get(idty_index).unwrap().next_scheduled != 0,
format!("Unconfirmed identity number {idty_index} should have a non-null next_scheduled value"),
);
}
}
}
/// check coherence between identity list and identity index hashmap
async fn verify_identity_coherence(
&mut self,
identities: &HashMap<IdtyIndex, IdtyValue>,
identity_index_of: &HashMap<[u8; 16], IdtyIndex>,
) {
// each identity should be correcly referenced in the hashmap
for (idty_index, idty_value) in identities {
// hash owner key to get key
let blake2_128_owner_key = &blake2_128(idty_value.owner_key.as_slice());
// get identity index from hashmap
if let Some(index_of) = identity_index_of.get(blake2_128_owner_key) {
self.assert(idty_index == index_of,
format!("identity number {idty_index} with owner key {0} is mapped to identity index {index_of}", idty_value.owner_key));
} else {
self.error(format!(
"identity with owner key {} is not present in hashmap",
idty_value.owner_key
));
}
}
}
}
}