Skip to content

Require exactly two signers on Solana sponsored creations - #5276

Open
tilacog wants to merge 2 commits into
mainfrom
tiago/be-525-require-exactly-two-signers-on-sponsored
Open

tilacog wants to merge 2 commits into
mainfrom
tiago/be-525-require-exactly-two-signers-on-sponsored

Conversation

@tilacog

@tilacog tilacog commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Description

validate read num_required_signatures from the client-controlled message header and used it only as a loop bound. A client could pad the header with throwaway keypairs that each signed, and the funder paid 5,000 lamports of base fee per extra signature. A sponsored creation now must name exactly two required signers, the funder and the owner.

Changes

  • create_order/mod.rs: validate returns InvalidTransaction ("a creation transaction must have exactly two required signers") when the header does not name exactly two required signers.
  • tests/api.rs: create_order_requires_the_owner_as_signer used a one-signer header, which the new rule refuses first. It now builds a two-signer creation with a stranger in the owner's slot and still expects InvalidSignature.

How to test

cargo nextest run -p solana-orderbook

  • a_header_without_exactly_two_required_signers_is_rejected refuses headers of 1 and 3.
  • create_order_rejects_an_extra_required_signer refuses a third signer with a valid signature.

Related Issues

BE-525

The funder pays the 5,000 lamport base fee for every required signature,
and the count comes from the client-controlled message header. Placement
gave it a minimum of two (funder and owner) and no maximum, so a client
could pad the header with throwaway signed keypairs and inflate the fee.
A creation whose header does not require exactly two signers is now
rejected with InvalidTransaction.
@linear-code

linear-code Bot commented Nov 13, 2026

Copy link
Copy Markdown

BE-525

@tilacog
tilacog marked this pull request as ready for review November 13, 2026 13:39
@tilacog
tilacog requested a review from a team as a code owner November 13, 2026 13:39
@claude

claude Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @tilacog's task in 22s —— View job


Review

  • Read the diff and validate in create_order/mod.rs
  • Check that the new rule fits with the owner-signer check and the signature loop
  • Run cargo nextest run -p solana-orderbook: not run, because this job isn't allowed to run cargo

I found nothing that needs a change. The check that the header names exactly two required signers runs right after the funder is confirmed as keys[0]. Together with the existing checks that the owner is not the funder and that the owner is among the required signers, this means the signer region is exactly [funder, owner]. So the loop that verifies signatures now always covers a single non-funder signature, and the funder can no longer be made to pay base fees for extra signers.

The tests look right:

  • create_order_requires_the_owner_as_signer still reaches the owner-signer check, because the stranger takes the second slot.
  • create_order_rejects_an_extra_required_signer gives a valid signature for every required signer, so it fails only because of the signer count.

@github-actions

Copy link
Copy Markdown

🤖 Claude reviewed this PR. Any issues are posted as inline comments on the relevant lines.

  • Runs automatically when a PR is opened or marked ready for review.
  • Re-run it: comment @claude on this PR (requires write access; bots and drafts are skipped).
  • Skip it: add the skip-claude-review label.

Won't run when: the PR is a draft, the author is a bot or lacks write access, or the skip-claude-review label is set.

@jmg-duarte jmg-duarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

}
if message.header().num_required_signatures != 2 {
return Err(PlacementError::InvalidTransaction(
"a creation transaction must have exactly two required signers",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a bit more info

Suggested change
"a creation transaction must have exactly two required signers",
"a creation transaction must have exactly two required signers: the funder and the owner",

or similar

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, plus maybe a comment/reminder on the reason for that.

Comment on lines +1139 to +1144
assert!(matches!(
validate(&sponsoring, &transaction, ValidationParameters::default()),
Err(PlacementError::InvalidTransaction(
"a creation transaction must have exactly two required signers"
))
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should now be possible

Suggested change
assert!(matches!(
validate(&sponsoring, &transaction, ValidationParameters::default()),
Err(PlacementError::InvalidTransaction(
"a creation transaction must have exactly two required signers"
))
));
assert_matches!(
validate(&sponsoring, &transaction, ValidationParameters::default()),
Err(PlacementError::InvalidTransaction(
..
))
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With .. this test probably passes without the new rule too. The message has no instructions, so validate already answers InvalidTransaction("the transaction carries no instructions"). assert_matches! is fine, but, IMO, the string should stay.

@squadgazzz squadgazzz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just nits

if keys.first() != Some(&sponsoring.funder) {
return Err(PlacementError::WrongFeePayer);
}
if message.header().num_required_signatures != 2 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A creation with a two-signer header and three signature slots still passes here, right?

Comment on lines +1139 to +1144
assert!(matches!(
validate(&sponsoring, &transaction, ValidationParameters::default()),
Err(PlacementError::InvalidTransaction(
"a creation transaction must have exactly two required signers"
))
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With .. this test probably passes without the new rule too. The message has no instructions, so validate already answers InvalidTransaction("the transaction carries no instructions"). assert_matches! is fine, but, IMO, the string should stay.

}
if message.header().num_required_signatures != 2 {
return Err(PlacementError::InvalidTransaction(
"a creation transaction must have exactly two required signers",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, plus maybe a comment/reminder on the reason for that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants