WeavingStates never reset, stale root finalizes into every new session with cross-session double mint

Hi team

Finding: pallet-weaver never resets WeavingStates between sessions. Once the first root finalizes, its hash is copied into the LoomStates of every later session, and the same EVM deposit can be claimed again once per replayed session.

Location: weaver lib dot rs. The weave_weft state update at lines 402 to 408 only replaces the stored weaving state when the new strand count is strictly greater than the stored count. The finalization branch in on_initialize at line 477 copies the stored hash into the current session LoomState whenever the stored count reaches the threshold, then removes the draft. No remove or kill site for WeavingStates exists at HEAD.

Impact: the trigger is ordinary honest operation. Installing the first root only needs the honest quorum doing its job. After that every later draft is consumed by the stale state, so no new gatekeeper root can ever be installed while the authority count stays at or below the first final count. That is liveness death for all later sessions. The double mint leg is honestly capped: the receiver is bound in the leaf preimage, so only the original depositor profits, one extra mint of the same deposit per replayed session.

Reproduction: the test below asserts the correct behavior, so it fails on the vulnerable code. I ran it against the pinned checkout and it panics with STALE ROOT REPLAYED into new session without any attestation.

#[test]
fn poc_d_stale_root_must_not_finalize_new_session_without_attestations() {
    let pair1 = AuthorityPair::from_string("//Alice", None).expect("seed");
    let pair2 = AuthorityPair::from_string("//Bob", None).expect("seed");
    let pair3 = AuthorityPair::from_string("//Charlie", None).expect("seed");
    let network_id = 1u32;
    let data = NetworkDataBuilder::default().with_network_type(NetworkType::Evm).build();
    let authorities = vec![pair1, pair2, pair3];
    let networks = vec![(network_id, data, 0u64)];
    new_test_ext(authorities, networks).execute_with(|| {
        let old_root = H256::repeat_byte(0xAA);
        LoomStates::<TestRuntime>::insert(&network_id, &0u64, old_root);
        WeavingStates::<TestRuntime>::insert(&network_id, WeavingState { hash: old_root, count: 3, next_session: 1 });
        CurrentWeavingSession::<TestRuntime>::insert(&network_id, 1u64);
        let draft = TapestryDraftBuilder::default()
            .with_current_block(10u64).with_block_delay(10u64).with_median_external(100).build();
        TapestryDrafts::<TestRuntime>::insert(&network_id, draft);
        System::set_block_number(11u64);
        GhostWeaver::on_initialize(11u64);
        assert_eq!(LoomStates::<TestRuntime>::get(&network_id, &1u64), H256::default(),
            "STALE ROOT REPLAYED into new session without any attestation");
        assert_eq!(CurrentWeavingSession::<TestRuntime>::get(&network_id), 1u64,
            "session advanced without a new root");
    });
}

A second test proves the cross-session double mint with a genuine Sepolia leaf proof vector: the same deposit leaf minted once under session zero and again under session one, because the EVM unique key is session scoped while the leaf preimage carries no session binding.

Suggested fix: reset WeavingStates at session finalization. This must ship in the same change as the preimage binding fix and the pool gate fix, or all three in one change, because fixing the pool gate alone activates the companion legs.

Shahmeer

Thank you for your report!

It looks like a similar issue was posted in another thread. To avoid repeating myself, could you please read the answer there? In short, it looks like the mechanism may have been slightly misunderstood - or perhaps I’m the one reading your report incorrectly.

Please confirm whether you agree that your issue is not actionable based on my answer in the link above, or feel free to continue the thread if you still believe it’s vulnerable.