paritytech / paritytech/web3-storage

`checkpoint` counts signature entries, not distinct providers

Open
#388 0 comments 0 reactions 1 assignee View on GitHub

@ilchu is already working on this.

Since Sep 4, 2026.

Dominant language
Rust
Stars
12
Forks
3
Avg merge
2d 2h
Merged PRs (30d)
33

Description

Motivation

min_providers is a bucket's redundancy policy: how many primary providers must
acknowledge a state before it becomes the canonical snapshot. The threshold counts
signature entries, so one provider's signature repeated N times satisfies a
policy of N — the setting is met with zero actual redundancy, the one property it
exists to guarantee.

The design already specifies the mechanism that prevents this; this one path
doesn't use it.

Current state

crates/pallets/storage-provider/src/lib.rscheckpoint counts entries in
signatures (signing_count += 1 per loop iteration) instead of counting the
primary_signers bitfield it builds alongside. No duplicate guard exists.

Reproduced against the pallet mock — two primaries, min_providers = 2, provider 2
signing twice, provider 3 never signing:

primary_providers = [2, 3]
min_providers = 2
checkpoint with duplicate signer -> Ok(())
count_signers() = 1 (bitfield [1])

The snapshot that lands is self-contradicting: its own bitfield says one signer
against a policy of two. The bucket-freeze path in the same file counts that
bitfield via count_signers() and would reject the snapshot checkpoint just
wrote.

The design (docs/design/scalable-web3-storage-implementation.md) says
min_providers is how many providers must acknowledge, "ensures minimum
redundancy", and specifies primary_signers with count_signers() /
has_provider_signed() helpers — both of which exist and are used elsewhere in the
pallet. Counting set bits is duplicate-free by construction.

Impact is bounded: checkpoint is writer-or-admin, so this is a writer
bypassing an admin's policy. The bitfield stays correct, so no provider is
wrongly slashable, and nothing touches a payment path — policy enforcement and
telemetry, not fund loss.

Pre-existing: identical on dev. Surfaced while reviewing #341, which touches
verify_signature but not this counting.

Needs double-check. Found during review; the reproduction above is
mock-only. Worth confirming on a real runtime, and sanity-checking the impact
claim (that liability and payments are genuinely unaffected) before deciding
priority.

Proposed solutions

  1. Count the bitfield via count_signers(), as the freeze path already does.
  2. Reject a repeated signer outright with a new DuplicateSigner error — also
    de-duplicates the event payload.

Either is a small change with no migration and no weight change.

Regression test

Currently returns Ok(()); should fail once fixed. For
crates/pallets/storage-provider/src/tests/checkpoint.rs (needs codec::Encode,
sp_core::{Pair as _, H256}, storage_primitives::{Commitment, CommitmentPayload}):

#[test]
fn checkpoint_rejects_repeated_signer() {
    new_test_ext().execute_with(|| {
        frame_system::Pallet::<Test>::set_block_number(1);

        register_provider(2, 200);
        register_provider(3, 200);
        let pair2 = provider_signer(2);
        let _pair3 = provider_signer(3);

        let bucket_id = setup_agreement(2, 1, 50, 200);
        add_primary_to_bucket(3, 1, bucket_id, 50);
        assert_ok!(StorageProvider::set_min_providers(
            RuntimeOrigin::signed(1),
            bucket_id,
            2
        ));

        let commitment = Commitment {
            mmr_root: H256::repeat_byte(0xAA),
            start_seq: 0,
            leaf_count: 10,
        };
        let payload = CommitmentPayload::new(bucket_id, commitment).encode();
        let sig = sp_runtime::MultiSignature::Sr25519(pair2.sign(&payload));

        // Provider 2 signs twice; provider 3 never signs.
        let sigs = vec![(2u64, sig.clone()), (2u64, sig)];
        assert_noop!(
            StorageProvider::checkpoint(
                RuntimeOrigin::signed(1),
                bucket_id,
                commitment,
                sigs.try_into().unwrap(),
            ),
            Error::<Test>::DuplicateSigner // option 1: InsufficientSignatures
        );
    });
}

TODO

  • Double-check the finding and the impact assessment
  • Pick option 1 or 2
  • Implement in checkpoint; keep count_signers() as the single counting idiom
  • Land the regression test above
  • Test: the emitted event lists each provider exactly once
  • Regression-guard extend_checkpoint: it ORs with no counter, so it is
    already correct — keep it that way if a counter is added
  • Update docs/reference/EXTRINSICS_REFERENCE.md if a new error is added

Related

  • #341 — multi-scheme provider keys (where this surfaced; does not touch this path)
  • docs/design/scalable-web3-storage-implementation.mdmin_providers semantics
    and the count_signers() / has_provider_signed() helpers

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.