Skip to content

Commit 74b6182

Browse files
committed
withlist to no pay only in success
1 parent 3e975f4 commit 74b6182

4 files changed

Lines changed: 96 additions & 40 deletions

File tree

pallets/signet/src/lib.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#![cfg_attr(not(feature = "std"), no_std)]
2+
#![allow(clippy::useless_conversion)]
23

34
use ethereum::{AccessListItem, EIP1559TransactionMessage, TransactionAction};
45
use frame_support::{
@@ -412,12 +413,12 @@ pub mod pallet {
412413

413414
/// Respond to signature requests (batch support)
414415
#[pallet::call_index(4)]
415-
#[pallet::weight((<T as Config>::WeightInfo::respond(), Pays::No))]
416+
#[pallet::weight(<T as Config>::WeightInfo::respond())]
416417
pub fn respond(
417418
origin: OriginFor<T>,
418419
request_ids: BoundedVec<[u8; 32], ConstU32<MAX_BATCH_SIZE>>,
419420
signatures: BoundedVec<Signature, ConstU32<MAX_BATCH_SIZE>>,
420-
) -> DispatchResult {
421+
) -> DispatchResultWithPostInfo {
421422
let responder = ensure_signed(origin)?;
422423
ensure!(Signers::<T>::contains_key(&responder), Error::<T>::NotAuthorizedSigner);
423424

@@ -431,16 +432,16 @@ pub mod pallet {
431432
});
432433
}
433434

434-
Ok(())
435+
Ok(Pays::No.into())
435436
}
436437

437438
/// Report signature generation errors (batch support)
438439
#[pallet::call_index(5)]
439-
#[pallet::weight((<T as Config>::WeightInfo::respond_error(), Pays::No))]
440+
#[pallet::weight(<T as Config>::WeightInfo::respond_error())]
440441
pub fn respond_error(
441442
origin: OriginFor<T>,
442443
errors: BoundedVec<ErrorResponse, ConstU32<MAX_BATCH_SIZE>>,
443-
) -> DispatchResult {
444+
) -> DispatchResultWithPostInfo {
444445
let responder = ensure_signed(origin)?;
445446
ensure!(Signers::<T>::contains_key(&responder), Error::<T>::NotAuthorizedSigner);
446447

@@ -452,18 +453,18 @@ pub mod pallet {
452453
});
453454
}
454455

455-
Ok(())
456+
Ok(Pays::No.into())
456457
}
457458

458459
/// Provide a read response with signature
459460
#[pallet::call_index(6)]
460-
#[pallet::weight((<T as Config>::WeightInfo::respond_bidirectional(), Pays::No))]
461+
#[pallet::weight(<T as Config>::WeightInfo::respond_bidirectional())]
461462
pub fn respond_bidirectional(
462463
origin: OriginFor<T>,
463464
request_id: [u8; 32],
464465
serialized_output: BoundedVec<u8, ConstU32<MAX_SERIALIZED_OUTPUT_LENGTH>>,
465466
signature: Signature,
466-
) -> DispatchResult {
467+
) -> DispatchResultWithPostInfo {
467468
let responder = ensure_signed(origin)?;
468469
ensure!(Signers::<T>::contains_key(&responder), Error::<T>::NotAuthorizedSigner);
469470

@@ -474,7 +475,7 @@ pub mod pallet {
474475
signature,
475476
});
476477

477-
Ok(())
478+
Ok(Pays::No.into())
478479
}
479480

480481
/// Pause the signet so that no new signing requests can be made.

pallets/signet/src/tests/signer_allowlist.rs

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -218,42 +218,89 @@ fn respond_should_fail_when_signer_is_removed() {
218218
}
219219

220220
// -----------------------------------------------------------------------------
221-
// feeless (Pays::No) annotation
221+
// fee: refunded on success, charged on failure
222222
// -----------------------------------------------------------------------------
223223

224224
#[test]
225-
fn respond_should_be_feeless_when_called() {
225+
fn respond_should_refund_fee_when_successful() {
226226
new_test_ext().execute_with(|| {
227-
let call = RuntimeCall::Signet(crate::Call::respond {
228-
request_ids: bounded_array::<100>(vec![[1u8; 32]]),
229-
signatures: bounded_sig::<100>(vec![create_test_signature()]),
230-
});
231-
assert_eq!(call.get_dispatch_info().pays_fee, Pays::No);
227+
assert_ok!(Signet::add_signer(RuntimeOrigin::root(), SIGNER));
228+
229+
let info = Signet::respond(
230+
RuntimeOrigin::signed(SIGNER),
231+
bounded_array::<100>(vec![[1u8; 32]]),
232+
bounded_sig::<100>(vec![create_test_signature()]),
233+
)
234+
.expect("respond should succeed");
235+
236+
assert_eq!(info.pays_fee, Pays::No);
232237
});
233238
}
234239

235240
#[test]
236-
fn respond_error_should_be_feeless_when_called() {
241+
fn respond_error_should_refund_fee_when_successful() {
237242
new_test_ext().execute_with(|| {
238-
let call = RuntimeCall::Signet(crate::Call::respond_error {
239-
errors: bounded_err::<100>(vec![ErrorResponse {
243+
assert_ok!(Signet::add_signer(RuntimeOrigin::root(), SIGNER));
244+
245+
let info = Signet::respond_error(
246+
RuntimeOrigin::signed(SIGNER),
247+
bounded_err::<100>(vec![ErrorResponse {
240248
request_id: [1u8; 32],
241249
error_message: bounded_u8::<1024>(b"boom".to_vec()),
242250
}]),
243-
});
244-
assert_eq!(call.get_dispatch_info().pays_fee, Pays::No);
251+
)
252+
.expect("respond_error should succeed");
253+
254+
assert_eq!(info.pays_fee, Pays::No);
245255
});
246256
}
247257

248258
#[test]
249-
fn respond_bidirectional_should_be_feeless_when_called() {
259+
fn respond_bidirectional_should_refund_fee_when_successful() {
250260
new_test_ext().execute_with(|| {
251-
let call = RuntimeCall::Signet(crate::Call::respond_bidirectional {
252-
request_id: [1u8; 32],
253-
serialized_output: bounded_u8::<65536>(b"out".to_vec()),
254-
signature: create_test_signature(),
255-
});
256-
assert_eq!(call.get_dispatch_info().pays_fee, Pays::No);
261+
assert_ok!(Signet::add_signer(RuntimeOrigin::root(), SIGNER));
262+
263+
let info = Signet::respond_bidirectional(
264+
RuntimeOrigin::signed(SIGNER),
265+
[1u8; 32],
266+
bounded_u8::<65536>(b"out".to_vec()),
267+
create_test_signature(),
268+
)
269+
.expect("respond_bidirectional should succeed");
270+
271+
assert_eq!(info.pays_fee, Pays::No);
272+
});
273+
}
274+
275+
#[test]
276+
fn respond_should_charge_fee_when_unauthorized() {
277+
new_test_ext().execute_with(|| {
278+
let err = Signet::respond(
279+
RuntimeOrigin::signed(OUTSIDER),
280+
bounded_array::<100>(vec![[1u8; 32]]),
281+
bounded_sig::<100>(vec![create_test_signature()]),
282+
)
283+
.expect_err("respond should fail for an unauthorized caller");
284+
285+
assert_eq!(err.error, Error::<Test>::NotAuthorizedSigner.into());
286+
assert_eq!(err.post_info.pays_fee, Pays::Yes);
287+
});
288+
}
289+
290+
#[test]
291+
fn respond_should_charge_fee_when_input_lengths_mismatch() {
292+
new_test_ext().execute_with(|| {
293+
assert_ok!(Signet::add_signer(RuntimeOrigin::root(), SIGNER));
294+
295+
let err = Signet::respond(
296+
RuntimeOrigin::signed(SIGNER),
297+
bounded_array::<100>(vec![[1u8; 32], [2u8; 32]]),
298+
bounded_sig::<100>(vec![create_test_signature()]),
299+
)
300+
.expect_err("respond should fail on length mismatch");
301+
302+
assert_eq!(err.error, Error::<Test>::InvalidInputLength.into());
303+
assert_eq!(err.post_info.pays_fee, Pays::Yes);
257304
});
258305
}
259306

scripts/signet-feeless-test/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# signet-feeless-test
22

3-
Chopsticks e2e for the allowlist-gated, feeless `signet.respond` flow: an
4-
authorized signer holding only the existential deposit (1 HDX) can call
5-
`respond` without paying a fee, and a non-allowlisted account is rejected.
3+
Chopsticks e2e for the allowlist-gated `signet.respond` flow: the fee is locked
4+
upfront and refunded when the call succeeds, otherwise charged. An authorized
5+
signer's successful `respond` costs nothing (fee refunded), while a
6+
non-allowlisted account fails and is charged. The caller must hold enough HDX to
7+
lock the fee.
68

79
Mirrors the pallet unit tests in `pallets/signet/src/tests/signer_allowlist.rs`.
810

scripts/signet-feeless-test/signet-feeless.test.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ const SIGNATURE = {
2020
recoveryId: 0,
2121
};
2222

23-
describe('signet feeless respond (allowlist-gated)', () => {
23+
// Existential deposit plus enough headroom to lock one respond fee.
24+
const FUND = ED + 10_000_000_000_000n;
25+
26+
describe('signet respond (lock / refund-on-success)', () => {
2427
let api: ApiPromise;
2528
let provider: WsProvider;
2629
let signer: KeyringPair;
@@ -34,9 +37,8 @@ describe('signet feeless respond (allowlist-gated)', () => {
3437

3538
await executeAsRootViaScheduler(api, provider, api.tx.signet.addSigner(signer.address));
3639

37-
// Fund both with exactly the existential deposit — no gas buffer.
38-
await setNativeBalance(provider, signer.address, ED);
39-
await setNativeBalance(provider, outsider.address, ED);
40+
await setNativeBalance(provider, signer.address, FUND);
41+
await setNativeBalance(provider, outsider.address, FUND);
4042
}, 600000);
4143

4244
afterAll(async () => {
@@ -48,12 +50,12 @@ describe('signet feeless respond (allowlist-gated)', () => {
4850
expect((entry as any).isSome).toBe(true);
4951
});
5052

51-
it('respond should be annotated Pays::No (zero partial fee)', async () => {
53+
it('respond should predict a non-zero fee (locked upfront)', async () => {
5254
const info = await api.tx.signet.respond([REQUEST_ID], [SIGNATURE]).paymentInfo(signer.address);
53-
expect((info as any).partialFee.toBigInt()).toBe(0n);
55+
expect((info as any).partialFee.toBigInt()).toBeGreaterThan(0n);
5456
});
5557

56-
it('respond should succeed for an ED-only signer and not charge a fee', async () => {
58+
it('respond should refund the fee when successful', async () => {
5759
const before = await freeBalance(api, signer.address);
5860

5961
const events = await submitAndMine(api, provider, api.tx.signet.respond([REQUEST_ID], [SIGNATURE]), signer);
@@ -63,10 +65,11 @@ describe('signet feeless respond (allowlist-gated)', () => {
6365

6466
const after = await freeBalance(api, signer.address);
6567
expect(after).toBe(before);
66-
expect(after).toBe(ED);
6768
}, 300000);
6869

69-
it('respond should be rejected for a non-allowlisted account', async () => {
70+
it('respond should charge the fee when the caller is not allowlisted', async () => {
71+
const before = await freeBalance(api, outsider.address);
72+
7073
const events = await submitAndMine(api, provider, api.tx.signet.respond([REQUEST_ID], [SIGNATURE]), outsider);
7174

7275
const failed = findEvent(events, 'system', 'ExtrinsicFailed');
@@ -75,5 +78,8 @@ describe('signet feeless respond (allowlist-gated)', () => {
7578
const err = moduleErrorName(api, failed);
7679
expect(err.section).toBe('signet');
7780
expect(err.name).toBe('NotAuthorizedSigner');
81+
82+
const after = await freeBalance(api, outsider.address);
83+
expect(after).toBeLessThan(before);
7884
}, 300000);
7985
});

0 commit comments

Comments
 (0)