Skip to content

Duplicate proposals catch in ns1r#642

Merged
arminsabouri merged 2 commits intopayjoin:masterfrom
benalleng:duplicate_proposal
Apr 28, 2025
Merged

Duplicate proposals catch in ns1r#642
arminsabouri merged 2 commits intopayjoin:masterfrom
benalleng:duplicate_proposal

Conversation

@benalleng
Copy link
Collaborator

@benalleng benalleng commented Apr 10, 2025

When building a multiparty proposal we did not have a catch to make sure that the proposals were all unique and as such a multiparty could be built by just adding the same proposal over and over. This creates a new error IdenticalProposals that checks to make sure the contexts are not matching when adding to a multiparty.

Closes #640

@benalleng benalleng changed the title Duplicate proposals in ns1r Duplicate proposals catch in ns1r Apr 10, 2025
@benalleng benalleng force-pushed the duplicate_proposal branch from 1a26e6e to a6259b0 Compare April 10, 2025 20:16
@coveralls
Copy link
Collaborator

coveralls commented Apr 10, 2025

Pull Request Test Coverage Report for Build 14669174853

Details

  • 116 of 136 (85.29%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage increased (+0.1%) to 82.057%

Changes Missing Coverage Covered Lines Changed/Added Lines %
payjoin/src/receive/multiparty/error.rs 13 14 92.86%
payjoin/src/receive/multiparty/mod.rs 89 108 82.41%
Totals Coverage Status
Change from base Build 14668777896: 0.1%
Covered Lines: 5433
Relevant Lines: 6621

💛 - Coveralls

@benalleng benalleng force-pushed the duplicate_proposal branch 3 times, most recently from 18104a2 to 3d19905 Compare April 11, 2025 12:09
@arminsabouri arminsabouri self-requested a review April 14, 2025 12:57
@benalleng benalleng force-pushed the duplicate_proposal branch 6 times, most recently from 3a220fc to 93d5518 Compare April 16, 2025 14:15
@benalleng benalleng marked this pull request as ready for review April 16, 2025 14:51
@benalleng benalleng force-pushed the duplicate_proposal branch from 93d5518 to c563761 Compare April 16, 2025 19:17
Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

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

Core logic looks correct to me. I just had a couple nits. Mainly around test related code.
Thanks for picking this one up 🚀

e: None,
});

pub(crate) static MULTIPARTY_CONTEXT: Lazy<SessionContext> = Lazy::new(|| SessionContext {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there anything that makes this MULTIPARTY specific? If not maybe we label this SHARED_CONTEXT_2 .
Or should we have a common test util to create hpke session contexts?

Comment on lines 303 to 305
pub const MULTIPARTY_SENDER_ONE: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAQCaAgAAAAKa/zFrWjicRDNVCehGj/2jdXGKuZLMzLNUo+uj7PBW8wAAAAAA/v///4HkRlmU9FA3sHckkCQMjGrabNpgAw37XaqUItNFjR5rAAAAAAD+////AgDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpLg6QUqAQAAABYAFH6MyygysrAmk/nunEFuGGDLf244aAAAAAEBHwDyBSoBAAAAFgAUGPI/E9xCHmaL6DIsjkaD8LX2mpIBCGsCRzBEAiBLJUhV7tja6FdELXDcB6Q3Gd+BMBpkjdHpj3DLnbL6AAIgJmFl3kpWBzkO8yDDl73BMMalSlAOQvfY+hqFRos5DhQBIQLfeq7CCftNEUHcRLZniJOqcgKZEqPc1A40BnEiZHj3FQABAJoCAAAAAp3/WZGMnqydV72jQR8wzxsgTF2XB9qKDWP8mdS6mohbAQAAAAD+////1xNypiNKcl215SfdF7Xa8W5VvnE/KliF4fN7vhYFbwQBAAAAAP7///8CoNkFKgEAAAAWABQchAe9uxzqT1EjLx4jgx9u1Mn6QADyBSoBAAAAFgAUye8yXWX0MvouXYhAFb06xX5kADpoAAAAAQEfAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOgEIawJHMEQCIF7ihY/YtUPUTOaEdJbg0/HiwKunK398BI67/LknPGqMAiBHBXmL6gTP8PxEGeWswk6T0tCI2Gvwq1zh+wd7h8QCWAEhApM0w2WFlw0eg64Zp3PeyRmxl/7WGHUED8Ul/aX1FiTBAAAAAA==";

pub const MULTIPARTY_SENDER_TWO: &str = "cHNidP8BAMMCAAAAA9cTcqYjSnJdteUn3Re12vFuVb5xPypYheHze74WBW8EAAAAAAD+////6JHS6JgYdav7tr+Q5bWk95C2aA4AudqZnjeRF0hx33gBAAAAAP7///+d/1mRjJ6snVe9o0EfMM8bIExdlwfaig1j/JnUupqIWwAAAAAA/v///wJO4wtUAgAAABYAFGAEc6cWf7z5a97Xxulg+S7JZyg/kvEFKgEAAAAWABQexWFsGEdXxYNTJLJ1qBCO0nXELwAAAAAAAAEAmgIAAAACnf9ZkYyerJ1XvaNBHzDPGyBMXZcH2ooNY/yZ1LqaiFsBAAAAAP7////XE3KmI0pyXbXlJ90XtdrxblW+cT8qWIXh83u+FgVvBAEAAAAA/v///wKg2QUqAQAAABYAFByEB727HOpPUSMvHiODH27UyfpAAPIFKgEAAAAWABTJ7zJdZfQy+i5diEAVvTrFfmQAOmgAAAABAR8A8gUqAQAAABYAFMnvMl1l9DL6Ll2IQBW9OsV+ZAA6AQhrAkcwRAIgXuKFj9i1Q9RM5oR0luDT8eLAq6crf3wEjrv8uSc8aowCIEcFeYvqBM/w/EQZ5azCTpPS0IjYa/CrXOH7B3uHxAJYASECkzTDZYWXDR6Drhmnc97JGbGX/tYYdQQPxSX9pfUWJMEAAQCaAgAAAAIKOB8lY4eoEupDnxviz0/nAuR2biNFKbdkvckiW5ioPAAAAAAA/v///w0g/mj67592vy29xhnZMGeVtEXN1jD4lU/SMZM8oStqAAAAAAD+////AgDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R36/g6QUqAQAAABYAFMGlw3hwcx1b+KQGWIfOUxzwrwWkaAAAAAEBHwDyBSoBAAAAFgAUC3r4YzVSpsWp3knMxbgWIx2R368BCGsCRzBEAiA//JH+jonzbzqnKI0Uti16iJcdsXI+6Zu0IAZKlOq6AwIgP0XawCCH73uXKilFqSXQQQrBvmi/Sx44D/A+/MQ/mJYBIQIekOyEpJKpFQd7eHuY6Vt4Qlf0+00Wp529I23hl/EpcQAAAA==";
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: If these are PSBTs I would add a _PSBT suffix. Additionally, instead of SENDER I would use ORIGINAL to match the pattern in the rest of this file. Original implies sender psbt
e.g
MULTIPARTY_ORIGINAL_PSBT_ONE.

}
}

fn multiparty_proposal_two() -> v1::UncheckedProposal {
Copy link
Collaborator

Choose a reason for hiding this comment

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

More of a question: is it worth it to just consolidate this method with the one above and return a Vec of test vectors? I only say this because they seem to share some boilerplate code.

fn multiparty_proposals() -> Vec<v1::UncheckedProposal> {
    let query = "v=2&optimisticmerge=true";
    let pairs = url::form_urlencoded::parse(query.as_bytes());
    let params = Params::from_query_pairs(pairs, SUPPORTED_VERSIONS)
        .expect("Could not parse from query pairs");

    [MULTIPARTY_SENDER_ONE, MULTIPARTY_SENDER_TWO]
        .iter()
        .map(|psbt_str| v1::UncheckedProposal {
            psbt: Psbt::from_str(psbt_str).expect("known psbt should parse"),
            params,
        })
        .collect()
}

@arminsabouri
Copy link
Collaborator

CI failures seems related to #658 which just got merged. So this branch just needs a rebase

@benalleng benalleng force-pushed the duplicate_proposal branch from c563761 to 459e657 Compare April 17, 2025 20:50
Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

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

LGMT. Just one small comment about some commented out code.

@benalleng
Copy link
Collaborator Author

Whoops, good catch.

As for the failing CI should we not display the session context in the error?

@arminsabouri
Copy link
Collaborator

Whoops, good catch.

As for the failing CI should we not display the session context in the error?

Perhaps just a top level identifier would work fine. The short id should work fine

@benalleng benalleng force-pushed the duplicate_proposal branch 2 times, most recently from 10093ba to 58cb52b Compare April 18, 2025 16:55
@benalleng
Copy link
Collaborator Author

I kept the SessionContext as what is displayed, I just no longer leak it as accessible in the pub error interface and instead just stringify it

@arminsabouri
Copy link
Collaborator

I kept the SessionContext as what is displayed, I just no longer leak it as accessible in the pub error interface and instead just stringify it

The SessionContext does include the hpke keypair. Can you confirm that secret key material is redacted when its stringified.

"Two sender psbts are identical\n left: {}\n right: {}",
current_psbt, incoming_psbt
),
#[cfg(feature = "v2")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this feature flag?

@benalleng
Copy link
Collaborator Author

benalleng commented Apr 20, 2025

yes the hpke keypair is redacted in the stringify method. But the symmetric pk is not... I think I am just not going to expose the session context after all, what do you think?

More than one identical participant: Two sender contexts are identical
 left: SessionContext { address: tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4, directory: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("exampl
e.com")), port: None, path: "/", query: None, fragment: None }, subdirectory: None, ohttp_keys: OhttpKeys(KeyConfig { key_id: 1, kem: K256Sha256, symmetric: [SymmetricSuite { kdf: HkdfSha256,
 aead: ChaCha20Poly1305 }], sk: Some(PrivateKey 83862a342f732d7d1a97548f501ce02021fbbfdaf5af46ccd8e729d5b006bc90), pk: PublicKey 04d14d8a71d2109c7510063572f321772abb167be99e719628a3fe3250f460
6a8a86569320931680ef34506ddb9987c260c8bd9df5baff62bf5c52b60f4d93cf8e }), expiry: SystemTime { tv_sec: 1745181272, tv_nsec: 721110968 }, s: HpkeKeyPair(SecpHpkeSecretKey([REDACTED]), SecpHpkeP
ublicKey(PublicKey(PublicKey(b3934c5d680367a6e0b9d9b800f2d21b463414d63feca22a7613a8297d4ff2bbcf85ed1ceddf54a2e26257cbf54e03e1f3f2fe64bb6c085f9242c542b8213588)))), e: None }
 right: SessionContext { address: tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4, directory: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("examp
le.com")), port: None, path: "/", query: None, fragment: None }, subdirectory: None, ohttp_keys: OhttpKeys(KeyConfig { key_id: 1, kem: K256Sha256, symmetric: [SymmetricSuite { kdf: HkdfSha256
, aead: ChaCha20Poly1305 }], sk: Some(PrivateKey 83862a342f732d7d1a97548f501ce02021fbbfdaf5af46ccd8e729d5b006bc90), pk: PublicKey 04d14d8a71d2109c7510063572f321772abb167be99e719628a3fe3250f46
06a8a86569320931680ef34506ddb9987c260c8bd9df5baff62bf5c52b60f4d93cf8e }), expiry: SystemTime { tv_sec: 1745181272, tv_nsec: 721110968 }, s: HpkeKeyPair(SecpHpkeSecretKey([REDACTED]), SecpHpke
PublicKey(PublicKey(PublicKey(b3934c5d680367a6e0b9d9b800f2d21b463414d63feca22a7613a8297d4ff2bbcf85ed1ceddf54a2e26257cbf54e03e1f3f2fe64bb6c085f9242c542b8213588)))), e: None }

@benalleng benalleng force-pushed the duplicate_proposal branch from 58cb52b to 2e4baf2 Compare April 20, 2025 20:35
@arminsabouri
Copy link
Collaborator

yes the hpke keypair is redacted in the stringify method. But the symmetric pk is not... I think I am just not going to expose the session context after all, what do you think?

More than one identical participant: Two sender contexts are identical
 left: SessionContext { address: tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4, directory: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("exampl
e.com")), port: None, path: "/", query: None, fragment: None }, subdirectory: None, ohttp_keys: OhttpKeys(KeyConfig { key_id: 1, kem: K256Sha256, symmetric: [SymmetricSuite { kdf: HkdfSha256,
 aead: ChaCha20Poly1305 }], sk: Some(PrivateKey 83862a342f732d7d1a97548f501ce02021fbbfdaf5af46ccd8e729d5b006bc90), pk: PublicKey 04d14d8a71d2109c7510063572f321772abb167be99e719628a3fe3250f460
6a8a86569320931680ef34506ddb9987c260c8bd9df5baff62bf5c52b60f4d93cf8e }), expiry: SystemTime { tv_sec: 1745181272, tv_nsec: 721110968 }, s: HpkeKeyPair(SecpHpkeSecretKey([REDACTED]), SecpHpkeP
ublicKey(PublicKey(PublicKey(b3934c5d680367a6e0b9d9b800f2d21b463414d63feca22a7613a8297d4ff2bbcf85ed1ceddf54a2e26257cbf54e03e1f3f2fe64bb6c085f9242c542b8213588)))), e: None }
 right: SessionContext { address: tb1q6d3a2w975yny0asuvd9a67ner4nks58ff0q8g4, directory: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("examp
le.com")), port: None, path: "/", query: None, fragment: None }, subdirectory: None, ohttp_keys: OhttpKeys(KeyConfig { key_id: 1, kem: K256Sha256, symmetric: [SymmetricSuite { kdf: HkdfSha256
, aead: ChaCha20Poly1305 }], sk: Some(PrivateKey 83862a342f732d7d1a97548f501ce02021fbbfdaf5af46ccd8e729d5b006bc90), pk: PublicKey 04d14d8a71d2109c7510063572f321772abb167be99e719628a3fe3250f46
06a8a86569320931680ef34506ddb9987c260c8bd9df5baff62bf5c52b60f4d93cf8e }), expiry: SystemTime { tv_sec: 1745181272, tv_nsec: 721110968 }, s: HpkeKeyPair(SecpHpkeSecretKey([REDACTED]), SecpHpke
PublicKey(PublicKey(PublicKey(b3934c5d680367a6e0b9d9b800f2d21b463414d63feca22a7613a8297d4ff2bbcf85ed1ceddf54a2e26257cbf54e03e1f3f2fe64bb6c085f9242c542b8213588)))), e: None }

I think short id is fine.

@benalleng benalleng force-pushed the duplicate_proposal branch from 2e4baf2 to 88591af Compare April 23, 2025 15:39
@benalleng
Copy link
Collaborator Author

Ended up adding a new id() method for UncheckedProposal similar to Receiver::id() so we don't have to worry about exposing anything we shouldn't.

@benalleng benalleng force-pushed the duplicate_proposal branch from 88591af to 7550506 Compare April 24, 2025 18:49
@arminsabouri
Copy link
Collaborator

Lint is failing. Need #667 to go in first

The Receiver Struct has a way of getting the short id but the UncheckedProposal
does not. This adds a similar id method for getting the short id instead for the
session context instead of exposing the whole context.
@benalleng benalleng force-pushed the duplicate_proposal branch from 7550506 to d3108b3 Compare April 25, 2025 16:11
When building a multiparty proposal we did not have a catch to
make sure that the proposals were all unique and as such a multiparty
could be built by just adding the same proposal over and over.  This creates
a new error IdenticalProposals that checks to make sure the contexts are not
matching when adding to a multiparty.
@benalleng benalleng force-pushed the duplicate_proposal branch from d1efc03 to 587af2d Compare April 25, 2025 16:30
Copy link
Collaborator

@arminsabouri arminsabouri left a comment

Choose a reason for hiding this comment

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

Ack 587af2d
My only other peice of feedback would be to expose the short id method on session context instead of any particular receiver type state. This out of the scope of this PR but worth noting.

@arminsabouri arminsabouri merged commit 73fafa8 into payjoin:master Apr 28, 2025
7 checks passed
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.

NS1R proposal builder should not accept duplicate proposal

3 participants