From adaae9496ac89c9c015d047bade5b38833ad87bc Mon Sep 17 00:00:00 2001 From: Denis Angell Date: Sat, 12 Sep 2026 06:35:47 -0400 Subject: [PATCH] featureConfidentialVoting --- API-CHANGELOG.md | 5 + include/xrpl/protocol/ConfidentialTransfer.h | 85 +++ include/xrpl/protocol/Indexes.h | 24 + include/xrpl/protocol/LedgerFormats.h | 6 +- include/xrpl/protocol/TER.h | 5 + include/xrpl/protocol/TxFlags.h | 4 + include/xrpl/protocol/detail/features.macro | 1 + .../xrpl/protocol/detail/ledger_entries.macro | 45 ++ include/xrpl/protocol/detail/sfields.macro | 16 + .../xrpl/protocol/detail/transactions.macro | 58 ++ include/xrpl/protocol/jss.h | 1 + .../protocol_autogen/ledger_entries/Ballot.h | 573 +++++++++++++++ .../ledger_entries/BallotVote.h | 373 ++++++++++ .../protocol_autogen/ledger_entries/MPToken.h | 70 ++ .../ledger_entries/MPTokenIssuance.h | 35 + .../transactions/BallotCastVote.h | 347 ++++++++++ .../transactions/BallotCreate.h | 399 +++++++++++ .../transactions/BallotDelete.h | 131 ++++ .../transactions/BallotFinalize.h | 179 +++++ include/xrpl/tx/invariants/BallotInvariant.h | 72 ++ include/xrpl/tx/invariants/InvariantCheck.h | 4 +- .../tx/transactors/voting/BallotCastVote.h | 73 ++ .../xrpl/tx/transactors/voting/BallotCreate.h | 69 ++ .../xrpl/tx/transactors/voting/BallotDelete.h | 58 ++ .../tx/transactors/voting/BallotFinalize.h | 62 ++ src/libxrpl/ledger/helpers/TokenHelpers.cpp | 37 + src/libxrpl/protocol/ConfidentialTransfer.cpp | 116 ++++ src/libxrpl/protocol/Indexes.cpp | 14 + src/libxrpl/protocol/InnerObjectFormats.cpp | 18 + src/libxrpl/protocol/TER.cpp | 5 + src/libxrpl/tx/invariants/BallotInvariant.cpp | 94 +++ .../tx/transactors/voting/BallotCastVote.cpp | 483 +++++++++++++ .../tx/transactors/voting/BallotCreate.cpp | 253 +++++++ .../tx/transactors/voting/BallotDelete.cpp | 119 ++++ .../tx/transactors/voting/BallotFinalize.cpp | 150 ++++ src/test/app/ConfidentialVoting_test.cpp | 652 ++++++++++++++++++ src/test/jtx/Ballot.h | 339 +++++++++ .../ledger_entries/BallotTests.cpp | 520 ++++++++++++++ .../ledger_entries/BallotVoteTests.cpp | 351 ++++++++++ .../ledger_entries/MPTokenIssuanceTests.cpp | 27 + .../ledger_entries/MPTokenTests.cpp | 54 ++ .../transactions/BallotCastVoteTests.cpp | 312 +++++++++ .../transactions/BallotCreateTests.cpp | 348 ++++++++++ .../transactions/BallotDeleteTests.cpp | 146 ++++ .../transactions/BallotFinalizeTests.cpp | 178 +++++ src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp | 45 ++ 46 files changed, 6954 insertions(+), 2 deletions(-) create mode 100644 include/xrpl/protocol_autogen/ledger_entries/Ballot.h create mode 100644 include/xrpl/protocol_autogen/ledger_entries/BallotVote.h create mode 100644 include/xrpl/protocol_autogen/transactions/BallotCastVote.h create mode 100644 include/xrpl/protocol_autogen/transactions/BallotCreate.h create mode 100644 include/xrpl/protocol_autogen/transactions/BallotDelete.h create mode 100644 include/xrpl/protocol_autogen/transactions/BallotFinalize.h create mode 100644 include/xrpl/tx/invariants/BallotInvariant.h create mode 100644 include/xrpl/tx/transactors/voting/BallotCastVote.h create mode 100644 include/xrpl/tx/transactors/voting/BallotCreate.h create mode 100644 include/xrpl/tx/transactors/voting/BallotDelete.h create mode 100644 include/xrpl/tx/transactors/voting/BallotFinalize.h create mode 100644 src/libxrpl/tx/invariants/BallotInvariant.cpp create mode 100644 src/libxrpl/tx/transactors/voting/BallotCastVote.cpp create mode 100644 src/libxrpl/tx/transactors/voting/BallotCreate.cpp create mode 100644 src/libxrpl/tx/transactors/voting/BallotDelete.cpp create mode 100644 src/libxrpl/tx/transactors/voting/BallotFinalize.cpp create mode 100644 src/test/app/ConfidentialVoting_test.cpp create mode 100644 src/test/jtx/Ballot.h create mode 100644 src/tests/libxrpl/protocol_autogen/ledger_entries/BallotTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/ledger_entries/BallotVoteTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/BallotCastVoteTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/BallotCreateTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/BallotDeleteTests.cpp create mode 100644 src/tests/libxrpl/protocol_autogen/transactions/BallotFinalizeTests.cpp diff --git a/API-CHANGELOG.md b/API-CHANGELOG.md index ed273120231..2549cad3616 100644 --- a/API-CHANGELOG.md +++ b/API-CHANGELOG.md @@ -62,6 +62,11 @@ This release contains bug fixes only and no API changes. ### Additions in 3.2.0 +- `ledger_entry`, `account_objects`: Added the `Ballot` and `BallotVote` ledger entry types introduced by the `ConfidentialVoting` amendment. `ledger_entry` accepts a `ballot` request object (`owner` + `seq`) and a `ballot_vote` request object (`ballot_id` + `account`), or a hex object ID for either. `account_objects` returns these entries and accepts them as `type` filters. + +- `account_tx`: Added an optional `delegate` request object to filter delegated transactions. The object requires `delegate_filter`, which must be either `actor` for transactions owned by the requested account but signed by another account, or `authorizer` for transactions signed by the requested account on behalf of another account. The optional `counter_party` account narrows the results to a specific signer/delegate for `actor` or a specific owner/delegator for `authorizer`. Malformed `delegate`, `delegate_filter`, and `counter_party` values return standard invalid field errors, and invalid account IDs return `actMalformed`. + When paginating delegate-filtered queries, a marker from a delegate-filtered query includes a `delegate` flag and is only valid for follow-up requests that also supply `delegate` (mixing marker conventions returns `invalidParams`). Because filtering is applied after the ledger scan, a page may contain fewer results than `limit` (possibly zero) while still returning a marker, so callers must continue until no marker is present. + - `ledger_entry`, `account_objects`: The `Delegate` ledger entry now includes an optional `DestinationNode` field, which stores the index into the authorized account's owner directory. This field is present on entries created after bidirectional directory tracking was introduced and may appear in RPC responses for those entries. ([#6681](https://github.com/XRPLF/rippled/pull/6681)) - `server_definitions`: Added the following new sections to the response ([#6321](https://github.com/XRPLF/rippled/pull/6321)): - `TRANSACTION_FORMATS`: Describes the fields and their optionality for each transaction type, including common fields shared across all transactions. diff --git a/include/xrpl/protocol/ConfidentialTransfer.h b/include/xrpl/protocol/ConfidentialTransfer.h index ecf7970aba0..3154b28d5f8 100644 --- a/include/xrpl/protocol/ConfidentialTransfer.h +++ b/include/xrpl/protocol/ConfidentialTransfer.h @@ -16,6 +16,7 @@ #include #include #include +#include namespace xrpl { @@ -433,4 +434,88 @@ verifyConvertBackProof( uint64_t amount, uint256 const& contextHash); +/** + * @brief Generates the context hash for a BallotCastVote transaction. + * + * Binds the cast's range proof to this specific transaction, preventing + * proof reuse across ballots or accounts. + * + * @param account The voter's account ID. + * @param ballotID The target ballot's ledger index. + * @param sequence The transaction sequence number or ticket number. + * @return A 256-bit context hash unique to this cast. + */ +uint256 +getBallotCastContextHash(AccountID const& account, uint256 const& ballotID, std::uint32_t sequence); + +/** + * @brief Generates the context hash for a BallotFinalize transaction. + * + * Binds each per-option decryption-correctness proof to this specific + * finalize transaction. + * + * @param account The tally authority's account ID. + * @param ballotID The target ballot's ledger index. + * @param sequence The transaction sequence number or ticket number. + * @return A 256-bit context hash unique to this finalize. + */ +uint256 +getBallotFinalizeContextHash( + AccountID const& account, + uint256 const& ballotID, + std::uint32_t sequence); + +/** + * @brief Verifies an aggregated Bulletproof range proof over ballot option + * commitments. + * + * Proves that every one of the N per-option values committed in + * @p commitments lies in the non-negative range, so a vote cannot subtract + * weight from a disliked option. Thin wrapper over + * mpt_verify_aggregated_bulletproof. + * + * @param proof The serialized aggregated Bulletproof. + * @param commitments One 33-byte Pedersen commitment per option. + * @param contextHash The 256-bit context hash binding the proof. + * @return tesSUCCESS if the proof is valid, or an error code otherwise. + */ +TER +verifyBallotRangeProof( + Slice const& proof, + std::vector const& commitments, + uint256 const& contextHash); + +/** + * @brief Verifies the ciphertext-commitment linkage for one ballot option. + * + * Proves that the option's ElGamal ciphertext(s) encrypt the same value that + * its Pedersen commitment commits to, and that every mirror ciphertext (tally, + * optional auditor, optional voter) encrypts that same value under shared + * randomness. Combined with the aggregated range proof over the commitment, + * this pins the tally update to a non-negative value the voter cannot forge — + * the verifiable-encryption guarantee a vote needs because it encrypts under + * the tally key, which the voter does not own. + * + * Wraps secp256k1_compact_standard_verify. The balance-linkage terms of that + * relation are neutralized with a canonical witness (sk_A = 1, rho_b = 1), so + * pk_A = G, PC_b = H and B1 = B2 = G are reconstructed identically here and by + * the prover, and only the 192-byte proof is carried on the wire. + * + * @param pubKeys The n mirror public keys, tally key first (33 bytes each). + * @param c1 The shared ElGamal C1 component (33 bytes). + * @param c2PerKey The n ElGamal C2 components, one per mirror key (33 bytes). + * @param commitment The option's Pedersen commitment PC_m (33 bytes). + * @param proof The 192-byte compact sigma linkage proof. + * @param contextHash The 256-bit context hash binding the proof. + * @return tesSUCCESS if the linkage holds, or an error code otherwise. + */ +TER +verifyBallotVoteLinkage( + std::vector const& pubKeys, + Slice const& c1, + std::vector const& c2PerKey, + Slice const& commitment, + Slice const& proof, + uint256 const& contextHash); + } // namespace xrpl diff --git a/include/xrpl/protocol/Indexes.h b/include/xrpl/protocol/Indexes.h index 0836cffaf73..5c0dddf1b1e 100644 --- a/include/xrpl/protocol/Indexes.h +++ b/include/xrpl/protocol/Indexes.h @@ -386,6 +386,30 @@ permissionedDomain(AccountID const& account, SeqProxy const& seq) noexcept; Keylet permissionedDomain(uint256 const& domainID) noexcept; + +/** + * A ballot owned by `owner`, keyed by the creating transaction sequence. + */ +Keylet +ballot(AccountID const& owner, std::uint32_t seq) noexcept; + +inline Keylet +ballot(uint256 const& ballotID) +{ + return {ltBALLOT, ballotID}; +} + +/** + * A voter's cast on the ballot identified by `ballotID`. + */ +Keylet +ballotVote(uint256 const& ballotID, AccountID const& voter) noexcept; + +inline Keylet +ballotVote(uint256 const& key) +{ + return {ltBALLOT_VOTE, key}; +} } // namespace keylet // Everything below is deprecated and should be removed in favor of keylets: diff --git a/include/xrpl/protocol/LedgerFormats.h b/include/xrpl/protocol/LedgerFormats.h index 68205e27e62..0cf7eaf7232 100644 --- a/include/xrpl/protocol/LedgerFormats.h +++ b/include/xrpl/protocol/LedgerFormats.h @@ -208,7 +208,11 @@ enum LedgerEntryType : std::uint16_t { \ LEDGER_OBJECT(Sponsorship, \ LSF_FLAG(lsfSponsorshipRequireSignForFee, 0x00010000) \ - LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) + LSF_FLAG(lsfSponsorshipRequireSignForReserve, 0x00020000)) \ + \ + LEDGER_OBJECT(Ballot, \ + LSF_FLAG(lsfBallotFinalized, 0x00000001) /* True, results have been published */ \ + LSF_FLAG(lsfVoterRecoverable, 0x00000002)) /* True, casts carry a voter self-recovery vector */ // clang-format on diff --git a/include/xrpl/protocol/TER.h b/include/xrpl/protocol/TER.h index 5702b01d1d3..fa63b25d934 100644 --- a/include/xrpl/protocol/TER.h +++ b/include/xrpl/protocol/TER.h @@ -377,6 +377,11 @@ enum TECcodes : TERUnderlyingType { tecNO_SPONSOR_PERMISSION = 200, tecOUT_OF_GAS = 201, tecBYTECODE_REJECTED = 202, + tecBALLOT_CLOSED = 205, + tecBALLOT_VOTED = 206, + tecBALLOT_NOT_OPEN = 207, + tecBALLOT_EXISTS = 208, + tecBALLOT_BAD_OPTIONS = 209, }; //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/TxFlags.h b/include/xrpl/protocol/TxFlags.h index 40edf2239ba..ca20fb5ea03 100644 --- a/include/xrpl/protocol/TxFlags.h +++ b/include/xrpl/protocol/TxFlags.h @@ -238,6 +238,10 @@ inline constexpr FlagValue tfUniversalMask = ~tfUniversal; TF_FLAG(tfSponsorshipEnd, 0x00010000) \ TF_FLAG(tfSponsorshipCreate, 0x00020000) \ TF_FLAG(tfSponsorshipReassign, 0x00040000), \ + MASK_ADJ(0)) \ + \ + TRANSACTION(BallotCreate, \ + TF_FLAG(tfVoterRecoverable, lsfVoterRecoverable), /* casts must carry a voter self-recovery vector */ \ MASK_ADJ(0)) // clang-format on diff --git a/include/xrpl/protocol/detail/features.macro b/include/xrpl/protocol/detail/features.macro index e63a7f515dc..c83bd13cb07 100644 --- a/include/xrpl/protocol/detail/features.macro +++ b/include/xrpl/protocol/detail/features.macro @@ -20,6 +20,7 @@ XRPL_FEATURE(LendingProtocolV1_2, Supported::No, VoteBehavior::DefaultN XRPL_FIX (Cleanup3_5_0, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(ConfidentialMPTKeyRotation, Supported::No, VoteBehavior::DefaultNo) XRPL_FIX (Cleanup3_4_0, Supported::Yes, VoteBehavior::DefaultNo) +XRPL_FEATURE(ConfidentialVoting, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(Sponsor, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(BatchV1_1, Supported::Yes, VoteBehavior::DefaultNo) XRPL_FEATURE(LendingProtocolV1_1, Supported::Yes, VoteBehavior::DefaultNo) diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 18c71b572cd..8eadfcf9d27 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -418,6 +418,7 @@ LEDGER_ENTRY(ltMPTOKEN_ISSUANCE, 0x007e, MPTokenIssuance, mpt_issuance, ({ {sfIssuerKeyEpoch, SoeOptional}, {sfAuditorKeyEpoch, SoeOptional}, {sfConfidentialOutstandingAmount, SoeDefault}, + {sfBallotID, SoeOptional}, // ConfidentialVoting: the issuance's open ballot, if any })) /** A ledger object which tracks MPToken @@ -437,6 +438,8 @@ LEDGER_ENTRY(ltMPTOKEN, 0x007f, MPToken, mptoken, ({ {sfIssuerEncryptedBalance, SoeOptional}, {sfAuditorEncryptedBalance, SoeOptional}, {sfHolderEncryptionKey, SoeOptional}, + {sfVoteLockedAmount, SoeOptional}, // ConfidentialVoting: locked while a ballot is open + {sfBallotID, SoeOptional}, // ConfidentialVoting: the ballot holding the lock })) /** A ledger object which tracks Oracle @@ -649,5 +652,47 @@ LEDGER_ENTRY(ltSPONSORSHIP, 0x0090, Sponsorship, sponsorship, ({ {sfSponseeNode, SoeRequired}, })) +/** A ledger object representing a confidential ballot with a homomorphically + encrypted tally. + + \sa keylet::ballot + */ +LEDGER_ENTRY(ltBALLOT, 0x0091, Ballot, ballot, ({ + {sfOwner, SoeRequired}, + {sfSequence, SoeRequired}, + {sfMPTokenIssuanceID, SoeOptional}, // token mode (one-of with sfDomainID) + {sfDomainID, SoeOptional}, // credential mode (one-of) + {sfDigest, SoeRequired}, // hash of the off-ledger ballot document + {sfURI, SoeOptional}, + {sfOptionCount, SoeRequired}, + {sfTallyPublicKey, SoeRequired}, + {sfAuditorEncryptionKey, SoeOptional}, // optional auditor ElGamal key + {sfEncryptedTally, SoeRequired}, // one ElGamal ciphertext per option + {sfOpenTime, SoeRequired}, + {sfCloseTime, SoeRequired}, + {sfVoteCount, SoeDefault}, + {sfResults, SoeOptional}, // plaintext counts, present after finalize + {sfOwnerNode, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, +})) + +/** A ledger object recording a single voter's cast on a Ballot. + + \sa keylet::ballotVote + */ +LEDGER_ENTRY(ltBALLOT_VOTE, 0x0092, BallotVote, ballot_vote, ({ + {sfAccount, SoeRequired}, // voter + {sfBallotID, SoeRequired}, + {sfBallotWeight, SoeRequired}, + {sfEncryptedVotes, SoeRequired}, // vote vector under the tally key + {sfAuditorEncryptedVotes, SoeOptional}, // present iff the ballot has an auditor key + {sfVoterPublicKey, SoeOptional}, // present iff lsfVoterRecoverable + {sfVoterEncryptedVotes, SoeOptional}, // present iff lsfVoterRecoverable + {sfOwnerNode, SoeRequired}, + {sfPreviousTxnID, SoeRequired}, + {sfPreviousTxnLgrSeq, SoeRequired}, +})) + #undef EXPAND #undef LEDGER_ENTRY_DUPLICATE diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index 2cf35743aea..0bf3b41c0de 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -28,6 +28,7 @@ TYPED_SFIELD(sfWasLockingChainSend, UINT8, 19) TYPED_SFIELD(sfWithdrawalPolicy, UINT8, 20) TYPED_SFIELD(sfContractResult, UINT8, 21) TYPED_SFIELD(sfVaultKind, UINT8, 22) +TYPED_SFIELD(sfOptionCount, UINT8, 23) // 16-bit integers (common) TYPED_SFIELD(sfLedgerEntryType, UINT16, 1, SField::kSmdNever) @@ -128,6 +129,8 @@ TYPED_SFIELD(sfBytecodeSizeLimit, UINT32, 82) TYPED_SFIELD(sfGasPrice, UINT32, 83) TYPED_SFIELD(sfGas, UINT32, 84) TYPED_SFIELD(sfGasUsed, UINT32, 85) +TYPED_SFIELD(sfOpenTime, UINT32, 89) +TYPED_SFIELD(sfVoteCount, UINT32, 90) // 64-bit integers (common) TYPED_SFIELD(sfIndexNext, UINT64, 1) @@ -161,6 +164,8 @@ TYPED_SFIELD(sfVaultNode, UINT64, 30) TYPED_SFIELD(sfLoanBrokerNode, UINT64, 31) TYPED_SFIELD(sfConfidentialOutstandingAmount, UINT64, 32, SField::kSmdBaseTen|SField::kSmdDefault) TYPED_SFIELD(sfSponseeNode, UINT64, 33) +TYPED_SFIELD(sfBallotWeight, UINT64, 34) +TYPED_SFIELD(sfVoteLockedAmount, UINT64, 35, SField::kSmdBaseTen|SField::kSmdDefault) // 128-bit TYPED_SFIELD(sfEmailHash, UINT128, 1) @@ -220,6 +225,7 @@ TYPED_SFIELD(sfLoanID, UINT256, 38) TYPED_SFIELD(sfReferenceHolding, UINT256, 39) TYPED_SFIELD(sfBlindingFactor, UINT256, 40) TYPED_SFIELD(sfObjectID, UINT256, 41) +TYPED_SFIELD(sfBallotID, UINT256, 43) // number (common) TYPED_SFIELD(sfNumber, NUMBER, 1) @@ -329,6 +335,9 @@ TYPED_SFIELD(sfAuditorEncryptionKey, VL, 44) TYPED_SFIELD(sfAmountCommitment, VL, 45) TYPED_SFIELD(sfBalanceCommitment, VL, 46) TYPED_SFIELD(sfBytecode, VL, 47) +TYPED_SFIELD(sfEncryptedVote, VL, 48) +TYPED_SFIELD(sfTallyPublicKey, VL, 49) +TYPED_SFIELD(sfVoterPublicKey, VL, 50) // account (common) TYPED_SFIELD(sfAccount, ACCOUNT, 1) @@ -421,6 +430,8 @@ UNTYPED_SFIELD(sfBatchSigner, OBJECT, 35) UNTYPED_SFIELD(sfBook, OBJECT, 36) UNTYPED_SFIELD(sfCounterpartySignature, OBJECT, 37, SField::kSmdDefault, SField::kNotSigning) UNTYPED_SFIELD(sfSponsorSignature, OBJECT, 38, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfBallotOption, OBJECT, 39) +UNTYPED_SFIELD(sfBallotResult, OBJECT, 40) // array of objects (common) // ARRAY/1 is reserved for end of array @@ -453,3 +464,8 @@ UNTYPED_SFIELD(sfAcceptedCredentials, ARRAY, 28) UNTYPED_SFIELD(sfPermissions, ARRAY, 29) UNTYPED_SFIELD(sfRawTransactions, ARRAY, 30) UNTYPED_SFIELD(sfBatchSigners, ARRAY, 31, SField::kSmdDefault, SField::kNotSigning) +UNTYPED_SFIELD(sfEncryptedTally, ARRAY, 32) +UNTYPED_SFIELD(sfEncryptedVotes, ARRAY, 33) +UNTYPED_SFIELD(sfAuditorEncryptedVotes, ARRAY, 34) +UNTYPED_SFIELD(sfVoterEncryptedVotes, ARRAY, 35) +UNTYPED_SFIELD(sfResults, ARRAY, 36) diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 454aa85ffd0..541c87663a0 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1134,6 +1134,64 @@ TRANSACTION(ttSPONSORSHIP_SET, 91, SponsorshipSet, {sfRemainingOwnerCountDelta, SoeOptional}, })) +/** This transaction type creates a confidential Ballot. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBALLOT_CREATE, 106, BallotCreate, + ({.amendment = featureConfidentialVoting}), + ({ + {sfMPTokenIssuanceID, SoeOptional}, + {sfDomainID, SoeOptional}, + {sfDigest, SoeRequired}, + {sfURI, SoeOptional}, + {sfOptionCount, SoeRequired}, + {sfTallyPublicKey, SoeRequired}, + {sfAuditorEncryptionKey, SoeOptional}, + {sfOpenTime, SoeRequired}, + {sfCloseTime, SoeRequired}, + {sfZKProof, SoeRequired}, +})) + +/** This transaction type casts an encrypted vote on a Ballot. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBALLOT_CAST_VOTE, 107, BallotCastVote, + ({.amendment = featureConfidentialVoting}), + ({ + {sfBallotID, SoeRequired}, + {sfEncryptedVotes, SoeRequired}, + {sfAuditorEncryptedVotes, SoeOptional}, + {sfVoterPublicKey, SoeOptional}, + {sfVoterEncryptedVotes, SoeOptional}, + {sfZKProof, SoeRequired}, + {sfBlindingFactor, SoeRequired}, + {sfCredentialIDs, SoeOptional}, +})) + +/** This transaction type publishes the verifiable results of a Ballot. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBALLOT_FINALIZE, 108, BallotFinalize, + ({.amendment = featureConfidentialVoting}), + ({ + {sfBallotID, SoeRequired}, + {sfResults, SoeRequired}, + {sfZKProof, SoeRequired}, +})) + +/** This transaction type deletes a Ballot or a BallotVote object. */ +#if TRANSACTION_INCLUDE +# include +#endif +TRANSACTION(ttBALLOT_DELETE, 109, BallotDelete, + ({.amendment = featureConfidentialVoting}), + ({ + {sfBallotID, SoeRequired}, +})) + /** This system-generated transaction type is used to update the status of the various amendments. For details, see: https://xrpl.org/amendments.html diff --git a/include/xrpl/protocol/jss.h b/include/xrpl/protocol/jss.h index 63e877ca311..8fa33310201 100644 --- a/include/xrpl/protocol/jss.h +++ b/include/xrpl/protocol/jss.h @@ -145,6 +145,7 @@ JSS(avg_bps_recv); // out: Peers JSS(avg_bps_sent); // out: Peers JSS(balance); // out: AccountLines JSS(balances); // out: GatewayBalances +JSS(ballot_id); // in: LedgerEntry JSS(base); // out: LogLevel JSS(base_asset); // in: get_aggregate_price JSS(base_fee); // out: NetworkOPs diff --git a/include/xrpl/protocol_autogen/ledger_entries/Ballot.h b/include/xrpl/protocol_autogen/ledger_entries/Ballot.h new file mode 100644 index 00000000000..9bd8d24cff7 --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/Ballot.h @@ -0,0 +1,573 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class BallotBuilder; + +/** + * @brief Ledger Entry: Ballot + * + * Type: ltBALLOT (0x0091) + * RPC Name: ballot + * + * Immutable wrapper around SLE providing type-safe field access. + * Use BallotBuilder to construct new ledger entries. + */ +class Ballot : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltBALLOT; + + /** + * @brief Construct a Ballot ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit Ballot(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for Ballot"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfOwner (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getOwner() const + { + return this->sle_->at(sfOwner); + } + + /** + * @brief Get sfSequence (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getSequence() const + { + return this->sle_->at(sfSequence); + } + + /** + * @brief Get sfMPTokenIssuanceID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMPTokenIssuanceID() const + { + if (hasMPTokenIssuanceID()) + return this->sle_->at(sfMPTokenIssuanceID); + return std::nullopt; + } + + /** + * @brief Check if sfMPTokenIssuanceID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMPTokenIssuanceID() const + { + return this->sle_->isFieldPresent(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfDomainID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDomainID() const + { + if (hasDomainID()) + return this->sle_->at(sfDomainID); + return std::nullopt; + } + + /** + * @brief Check if sfDomainID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDomainID() const + { + return this->sle_->isFieldPresent(sfDomainID); + } + + /** + * @brief Get sfDigest (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getDigest() const + { + return this->sle_->at(sfDigest); + } + + /** + * @brief Get sfURI (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getURI() const + { + if (hasURI()) + return this->sle_->at(sfURI); + return std::nullopt; + } + + /** + * @brief Check if sfURI is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasURI() const + { + return this->sle_->isFieldPresent(sfURI); + } + + /** + * @brief Get sfOptionCount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT8::type::value_type + getOptionCount() const + { + return this->sle_->at(sfOptionCount); + } + + /** + * @brief Get sfTallyPublicKey (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getTallyPublicKey() const + { + return this->sle_->at(sfTallyPublicKey); + } + + /** + * @brief Get sfAuditorEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptionKey() const + { + if (hasAuditorEncryptionKey()) + return this->sle_->at(sfAuditorEncryptionKey); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptionKey() const + { + return this->sle_->isFieldPresent(sfAuditorEncryptionKey); + } + + /** + * @brief Get sfEncryptedTally (SoeRequired) + * @note This is an untyped field (unknown). + * @return The field value. + */ + [[nodiscard]] + STArray const& + getEncryptedTally() const + { + return this->sle_->getFieldArray(sfEncryptedTally); + } + + /** + * @brief Get sfOpenTime (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getOpenTime() const + { + return this->sle_->at(sfOpenTime); + } + + /** + * @brief Get sfCloseTime (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getCloseTime() const + { + return this->sle_->at(sfCloseTime); + } + + /** + * @brief Get sfVoteCount (SoeDefault) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVoteCount() const + { + if (hasVoteCount()) + return this->sle_->at(sfVoteCount); + return std::nullopt; + } + + /** + * @brief Check if sfVoteCount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoteCount() const + { + return this->sle_->isFieldPresent(sfVoteCount); + } + + /** + * @brief Get sfResults (SoeOptional) + * @note This is an untyped field (unknown). + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getResults() const + { + if (this->sle_->isFieldPresent(sfResults)) + return this->sle_->getFieldArray(sfResults); + return std::nullopt; + } + + /** + * @brief Check if sfResults is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasResults() const + { + return this->sle_->isFieldPresent(sfResults); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } +}; + +/** + * @brief Builder for Ballot ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class BallotBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new BallotBuilder with required fields. + * @param owner The sfOwner field value. + * @param sequence The sfSequence field value. + * @param digest The sfDigest field value. + * @param optionCount The sfOptionCount field value. + * @param tallyPublicKey The sfTallyPublicKey field value. + * @param encryptedTally The sfEncryptedTally field value. + * @param openTime The sfOpenTime field value. + * @param closeTime The sfCloseTime field value. + * @param ownerNode The sfOwnerNode field value. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + */ + BallotBuilder(std::decay_t const& owner,std::decay_t const& sequence,std::decay_t const& digest,std::decay_t const& optionCount,std::decay_t const& tallyPublicKey,STArray const& encryptedTally,std::decay_t const& openTime,std::decay_t const& closeTime,std::decay_t const& ownerNode,std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq) + : LedgerEntryBuilderBase(ltBALLOT) + { + setOwner(owner); + setSequence(sequence); + setDigest(digest); + setOptionCount(optionCount); + setTallyPublicKey(tallyPublicKey); + setEncryptedTally(encryptedTally); + setOpenTime(openTime); + setCloseTime(closeTime); + setOwnerNode(ownerNode); + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + } + + /** + * @brief Construct a BallotBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + BallotBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltBALLOT) + { + throw std::runtime_error("Invalid ledger entry type for Ballot"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfOwner (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setOwner(std::decay_t const& value) + { + object_[sfOwner] = value; + return *this; + } + + /** + * @brief Set sfSequence (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setSequence(std::decay_t const& value) + { + object_[sfSequence] = value; + return *this; + } + + /** + * @brief Set sfMPTokenIssuanceID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfDomainID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setDomainID(std::decay_t const& value) + { + object_[sfDomainID] = value; + return *this; + } + + /** + * @brief Set sfDigest (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setDigest(std::decay_t const& value) + { + object_[sfDigest] = value; + return *this; + } + + /** + * @brief Set sfURI (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setURI(std::decay_t const& value) + { + object_[sfURI] = value; + return *this; + } + + /** + * @brief Set sfOptionCount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setOptionCount(std::decay_t const& value) + { + object_[sfOptionCount] = value; + return *this; + } + + /** + * @brief Set sfTallyPublicKey (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setTallyPublicKey(std::decay_t const& value) + { + object_[sfTallyPublicKey] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setAuditorEncryptionKey(std::decay_t const& value) + { + object_[sfAuditorEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfEncryptedTally (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setEncryptedTally(STArray const& value) + { + object_.setFieldArray(sfEncryptedTally, value); + return *this; + } + + /** + * @brief Set sfOpenTime (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setOpenTime(std::decay_t const& value) + { + object_[sfOpenTime] = value; + return *this; + } + + /** + * @brief Set sfCloseTime (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setCloseTime(std::decay_t const& value) + { + object_[sfCloseTime] = value; + return *this; + } + + /** + * @brief Set sfVoteCount (SoeDefault) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setVoteCount(std::decay_t const& value) + { + object_[sfVoteCount] = value; + return *this; + } + + /** + * @brief Set sfResults (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setResults(STArray const& value) + { + object_.setFieldArray(sfResults, value); + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Build and return the completed Ballot wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + Ballot + build(uint256 const& index) + { + return Ballot{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/BallotVote.h b/include/xrpl/protocol_autogen/ledger_entries/BallotVote.h new file mode 100644 index 00000000000..90eaf28a79c --- /dev/null +++ b/include/xrpl/protocol_autogen/ledger_entries/BallotVote.h @@ -0,0 +1,373 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::ledger_entries { + +class BallotVoteBuilder; + +/** + * @brief Ledger Entry: BallotVote + * + * Type: ltBALLOT_VOTE (0x0092) + * RPC Name: ballot_vote + * + * Immutable wrapper around SLE providing type-safe field access. + * Use BallotVoteBuilder to construct new ledger entries. + */ +class BallotVote : public LedgerEntryBase +{ +public: + static constexpr LedgerEntryType entryType = ltBALLOT_VOTE; + + /** + * @brief Construct a BallotVote ledger entry wrapper from an existing SLE object. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + explicit BallotVote(SLE::const_pointer sle) + : LedgerEntryBase(std::move(sle)) + { + // Verify ledger entry type + if (sle_->getType() != entryType) + { + throw std::runtime_error("Invalid ledger entry type for BallotVote"); + } + } + + // Ledger entry-specific field getters + + /** + * @brief Get sfAccount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_ACCOUNT::type::value_type + getAccount() const + { + return this->sle_->at(sfAccount); + } + + /** + * @brief Get sfBallotID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBallotID() const + { + return this->sle_->at(sfBallotID); + } + + /** + * @brief Get sfBallotWeight (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getBallotWeight() const + { + return this->sle_->at(sfBallotWeight); + } + + /** + * @brief Get sfEncryptedVotes (SoeRequired) + * @note This is an untyped field (unknown). + * @return The field value. + */ + [[nodiscard]] + STArray const& + getEncryptedVotes() const + { + return this->sle_->getFieldArray(sfEncryptedVotes); + } + + /** + * @brief Get sfAuditorEncryptedVotes (SoeOptional) + * @note This is an untyped field (unknown). + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getAuditorEncryptedVotes() const + { + if (this->sle_->isFieldPresent(sfAuditorEncryptedVotes)) + return this->sle_->getFieldArray(sfAuditorEncryptedVotes); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedVotes is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedVotes() const + { + return this->sle_->isFieldPresent(sfAuditorEncryptedVotes); + } + + /** + * @brief Get sfVoterPublicKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVoterPublicKey() const + { + if (hasVoterPublicKey()) + return this->sle_->at(sfVoterPublicKey); + return std::nullopt; + } + + /** + * @brief Check if sfVoterPublicKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoterPublicKey() const + { + return this->sle_->isFieldPresent(sfVoterPublicKey); + } + + /** + * @brief Get sfVoterEncryptedVotes (SoeOptional) + * @note This is an untyped field (unknown). + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getVoterEncryptedVotes() const + { + if (this->sle_->isFieldPresent(sfVoterEncryptedVotes)) + return this->sle_->getFieldArray(sfVoterEncryptedVotes); + return std::nullopt; + } + + /** + * @brief Check if sfVoterEncryptedVotes is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoterEncryptedVotes() const + { + return this->sle_->isFieldPresent(sfVoterEncryptedVotes); + } + + /** + * @brief Get sfOwnerNode (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT64::type::value_type + getOwnerNode() const + { + return this->sle_->at(sfOwnerNode); + } + + /** + * @brief Get sfPreviousTxnID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getPreviousTxnID() const + { + return this->sle_->at(sfPreviousTxnID); + } + + /** + * @brief Get sfPreviousTxnLgrSeq (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getPreviousTxnLgrSeq() const + { + return this->sle_->at(sfPreviousTxnLgrSeq); + } +}; + +/** + * @brief Builder for BallotVote ledger entries. + * + * Provides a fluent interface for constructing ledger entries with method chaining. + * Uses STObject internally for flexible ledger entry construction. + * Inherits common field setters from LedgerEntryBuilderBase. + */ +class BallotVoteBuilder : public LedgerEntryBuilderBase +{ +public: + /** + * @brief Construct a new BallotVoteBuilder with required fields. + * @param account The sfAccount field value. + * @param ballotID The sfBallotID field value. + * @param ballotWeight The sfBallotWeight field value. + * @param encryptedVotes The sfEncryptedVotes field value. + * @param ownerNode The sfOwnerNode field value. + * @param previousTxnID The sfPreviousTxnID field value. + * @param previousTxnLgrSeq The sfPreviousTxnLgrSeq field value. + */ + BallotVoteBuilder(std::decay_t const& account,std::decay_t const& ballotID,std::decay_t const& ballotWeight,STArray const& encryptedVotes,std::decay_t const& ownerNode,std::decay_t const& previousTxnID,std::decay_t const& previousTxnLgrSeq) + : LedgerEntryBuilderBase(ltBALLOT_VOTE) + { + setAccount(account); + setBallotID(ballotID); + setBallotWeight(ballotWeight); + setEncryptedVotes(encryptedVotes); + setOwnerNode(ownerNode); + setPreviousTxnID(previousTxnID); + setPreviousTxnLgrSeq(previousTxnLgrSeq); + } + + /** + * @brief Construct a BallotVoteBuilder from an existing SLE object. + * @param sle The existing ledger entry to copy from. + * @throws std::runtime_error if the ledger entry type doesn't match. + */ + BallotVoteBuilder(SLE::const_pointer sle) + { + if (sle->at(sfLedgerEntryType) != ltBALLOT_VOTE) + { + throw std::runtime_error("Invalid ledger entry type for BallotVote"); + } + object_ = *sle; + } + + /** + * @brief Ledger entry-specific field setters + */ + + /** + * @brief Set sfAccount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setAccount(std::decay_t const& value) + { + object_[sfAccount] = value; + return *this; + } + + /** + * @brief Set sfBallotID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setBallotID(std::decay_t const& value) + { + object_[sfBallotID] = value; + return *this; + } + + /** + * @brief Set sfBallotWeight (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setBallotWeight(std::decay_t const& value) + { + object_[sfBallotWeight] = value; + return *this; + } + + /** + * @brief Set sfEncryptedVotes (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfAuditorEncryptedVotes (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setAuditorEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfAuditorEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfVoterPublicKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setVoterPublicKey(std::decay_t const& value) + { + object_[sfVoterPublicKey] = value; + return *this; + } + + /** + * @brief Set sfVoterEncryptedVotes (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setVoterEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfVoterEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfOwnerNode (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setOwnerNode(std::decay_t const& value) + { + object_[sfOwnerNode] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setPreviousTxnID(std::decay_t const& value) + { + object_[sfPreviousTxnID] = value; + return *this; + } + + /** + * @brief Set sfPreviousTxnLgrSeq (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotVoteBuilder& + setPreviousTxnLgrSeq(std::decay_t const& value) + { + object_[sfPreviousTxnLgrSeq] = value; + return *this; + } + + /** + * @brief Build and return the completed BallotVote wrapper. + * @param index The ledger entry index. + * @return The constructed ledger entry wrapper. + */ + BallotVote + build(uint256 const& index) + { + return BallotVote{std::make_shared(std::move(object_), index)}; + } +}; + +} // namespace xrpl::ledger_entries diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h index 874d779d099..dce7827bd24 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPToken.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPToken.h @@ -291,6 +291,54 @@ class MPToken : public LedgerEntryBase { return this->sle_->isFieldPresent(sfHolderEncryptionKey); } + + /** + * @brief Get sfVoteLockedAmount (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVoteLockedAmount() const + { + if (hasVoteLockedAmount()) + return this->sle_->at(sfVoteLockedAmount); + return std::nullopt; + } + + /** + * @brief Check if sfVoteLockedAmount is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoteLockedAmount() const + { + return this->sle_->isFieldPresent(sfVoteLockedAmount); + } + + /** + * @brief Get sfBallotID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBallotID() const + { + if (hasBallotID()) + return this->sle_->at(sfBallotID); + return std::nullopt; + } + + /** + * @brief Check if sfBallotID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBallotID() const + { + return this->sle_->isFieldPresent(sfBallotID); + } }; /** @@ -482,6 +530,28 @@ class MPTokenBuilder : public LedgerEntryBuilderBase return *this; } + /** + * @brief Set sfVoteLockedAmount (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setVoteLockedAmount(std::decay_t const& value) + { + object_[sfVoteLockedAmount] = value; + return *this; + } + + /** + * @brief Set sfBallotID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + MPTokenBuilder& + setBallotID(std::decay_t const& value) + { + object_[sfBallotID] = value; + return *this; + } + /** * @brief Build and return the completed MPToken wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h index 74b5e3c4eb2..9a5b1bbb09e 100644 --- a/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h +++ b/include/xrpl/protocol_autogen/ledger_entries/MPTokenIssuance.h @@ -422,6 +422,30 @@ class MPTokenIssuance : public LedgerEntryBase { return this->sle_->isFieldPresent(sfConfidentialOutstandingAmount); } + + /** + * @brief Get sfBallotID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getBallotID() const + { + if (hasBallotID()) + return this->sle_->at(sfBallotID); + return std::nullopt; + } + + /** + * @brief Check if sfBallotID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasBallotID() const + { + return this->sle_->isFieldPresent(sfBallotID); + } }; /** @@ -681,6 +705,17 @@ class MPTokenIssuanceBuilder : public LedgerEntryBuilderBase const& value) + { + object_[sfBallotID] = value; + return *this; + } + /** * @brief Build and return the completed MPTokenIssuance wrapper. * @param index The ledger entry index. diff --git a/include/xrpl/protocol_autogen/transactions/BallotCastVote.h b/include/xrpl/protocol_autogen/transactions/BallotCastVote.h new file mode 100644 index 00000000000..c355b6698fa --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/BallotCastVote.h @@ -0,0 +1,347 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class BallotCastVoteBuilder; + +/** + * @brief Transaction: BallotCastVote + * + * Type: ttBALLOT_CAST_VOTE (107) + * Delegable: Delegation::NotDelegable + * Amendment: featureConfidentialVoting + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use BallotCastVoteBuilder to construct new transactions. + */ +class BallotCastVote : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttBALLOT_CAST_VOTE; + + /** + * @brief Construct a BallotCastVote transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit BallotCastVote(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for BallotCastVote"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfBallotID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBallotID() const + { + return this->tx_->at(sfBallotID); + } + /** + * @brief Get sfEncryptedVotes (SoeRequired) + * @note This is an untyped field. + * @return The field value. + */ + [[nodiscard]] + STArray const& + getEncryptedVotes() const + { + return this->tx_->getFieldArray(sfEncryptedVotes); + } + /** + * @brief Get sfAuditorEncryptedVotes (SoeOptional) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getAuditorEncryptedVotes() const + { + if (this->tx_->isFieldPresent(sfAuditorEncryptedVotes)) + return this->tx_->getFieldArray(sfAuditorEncryptedVotes); + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptedVotes is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptedVotes() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptedVotes); + } + + /** + * @brief Get sfVoterPublicKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getVoterPublicKey() const + { + if (hasVoterPublicKey()) + { + return this->tx_->at(sfVoterPublicKey); + } + return std::nullopt; + } + + /** + * @brief Check if sfVoterPublicKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoterPublicKey() const + { + return this->tx_->isFieldPresent(sfVoterPublicKey); + } + /** + * @brief Get sfVoterEncryptedVotes (SoeOptional) + * @note This is an untyped field. + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + std::optional> + getVoterEncryptedVotes() const + { + if (this->tx_->isFieldPresent(sfVoterEncryptedVotes)) + return this->tx_->getFieldArray(sfVoterEncryptedVotes); + return std::nullopt; + } + + /** + * @brief Check if sfVoterEncryptedVotes is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasVoterEncryptedVotes() const + { + return this->tx_->isFieldPresent(sfVoterEncryptedVotes); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } + + /** + * @brief Get sfBlindingFactor (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBlindingFactor() const + { + return this->tx_->at(sfBlindingFactor); + } + + /** + * @brief Get sfCredentialIDs (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getCredentialIDs() const + { + if (hasCredentialIDs()) + { + return this->tx_->at(sfCredentialIDs); + } + return std::nullopt; + } + + /** + * @brief Check if sfCredentialIDs is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasCredentialIDs() const + { + return this->tx_->isFieldPresent(sfCredentialIDs); + } +}; + +/** + * @brief Builder for BallotCastVote transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class BallotCastVoteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new BallotCastVoteBuilder with required fields. + * @param account The account initiating the transaction. + * @param ballotID The sfBallotID field value. + * @param encryptedVotes The sfEncryptedVotes field value. + * @param zKProof The sfZKProof field value. + * @param blindingFactor The sfBlindingFactor field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + BallotCastVoteBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& ballotID, STArray const& encryptedVotes, std::decay_t const& zKProof, std::decay_t const& blindingFactor, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttBALLOT_CAST_VOTE, account, sequence, fee) + { + setBallotID(ballotID); + setEncryptedVotes(encryptedVotes); + setZKProof(zKProof); + setBlindingFactor(blindingFactor); + } + + /** + * @brief Construct a BallotCastVoteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + BallotCastVoteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttBALLOT_CAST_VOTE) + { + throw std::runtime_error("Invalid transaction type for BallotCastVoteBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfBallotID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setBallotID(std::decay_t const& value) + { + object_[sfBallotID] = value; + return *this; + } + + /** + * @brief Set sfEncryptedVotes (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfAuditorEncryptedVotes (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setAuditorEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfAuditorEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfVoterPublicKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setVoterPublicKey(std::decay_t const& value) + { + object_[sfVoterPublicKey] = value; + return *this; + } + + /** + * @brief Set sfVoterEncryptedVotes (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setVoterEncryptedVotes(STArray const& value) + { + object_.setFieldArray(sfVoterEncryptedVotes, value); + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Set sfBlindingFactor (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setBlindingFactor(std::decay_t const& value) + { + object_[sfBlindingFactor] = value; + return *this; + } + + /** + * @brief Set sfCredentialIDs (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCastVoteBuilder& + setCredentialIDs(std::decay_t const& value) + { + object_[sfCredentialIDs] = value; + return *this; + } + + /** + * @brief Build and return the BallotCastVote wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + BallotCastVote + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return BallotCastVote{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/BallotCreate.h b/include/xrpl/protocol_autogen/transactions/BallotCreate.h new file mode 100644 index 00000000000..8ebe3adc53c --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/BallotCreate.h @@ -0,0 +1,399 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class BallotCreateBuilder; + +/** + * @brief Transaction: BallotCreate + * + * Type: ttBALLOT_CREATE (106) + * Delegable: Delegation::NotDelegable + * Amendment: featureConfidentialVoting + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use BallotCreateBuilder to construct new transactions. + */ +class BallotCreate : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttBALLOT_CREATE; + + /** + * @brief Construct a BallotCreate transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit BallotCreate(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for BallotCreate"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfMPTokenIssuanceID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getMPTokenIssuanceID() const + { + if (hasMPTokenIssuanceID()) + { + return this->tx_->at(sfMPTokenIssuanceID); + } + return std::nullopt; + } + + /** + * @brief Check if sfMPTokenIssuanceID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasMPTokenIssuanceID() const + { + return this->tx_->isFieldPresent(sfMPTokenIssuanceID); + } + + /** + * @brief Get sfDomainID (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getDomainID() const + { + if (hasDomainID()) + { + return this->tx_->at(sfDomainID); + } + return std::nullopt; + } + + /** + * @brief Check if sfDomainID is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasDomainID() const + { + return this->tx_->isFieldPresent(sfDomainID); + } + + /** + * @brief Get sfDigest (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getDigest() const + { + return this->tx_->at(sfDigest); + } + + /** + * @brief Get sfURI (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getURI() const + { + if (hasURI()) + { + return this->tx_->at(sfURI); + } + return std::nullopt; + } + + /** + * @brief Check if sfURI is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasURI() const + { + return this->tx_->isFieldPresent(sfURI); + } + + /** + * @brief Get sfOptionCount (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT8::type::value_type + getOptionCount() const + { + return this->tx_->at(sfOptionCount); + } + + /** + * @brief Get sfTallyPublicKey (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getTallyPublicKey() const + { + return this->tx_->at(sfTallyPublicKey); + } + + /** + * @brief Get sfAuditorEncryptionKey (SoeOptional) + * @return The field value, or std::nullopt if not present. + */ + [[nodiscard]] + protocol_autogen::Optional + getAuditorEncryptionKey() const + { + if (hasAuditorEncryptionKey()) + { + return this->tx_->at(sfAuditorEncryptionKey); + } + return std::nullopt; + } + + /** + * @brief Check if sfAuditorEncryptionKey is present. + * @return True if the field is present, false otherwise. + */ + [[nodiscard]] + bool + hasAuditorEncryptionKey() const + { + return this->tx_->isFieldPresent(sfAuditorEncryptionKey); + } + + /** + * @brief Get sfOpenTime (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getOpenTime() const + { + return this->tx_->at(sfOpenTime); + } + + /** + * @brief Get sfCloseTime (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT32::type::value_type + getCloseTime() const + { + return this->tx_->at(sfCloseTime); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } +}; + +/** + * @brief Builder for BallotCreate transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class BallotCreateBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new BallotCreateBuilder with required fields. + * @param account The account initiating the transaction. + * @param digest The sfDigest field value. + * @param optionCount The sfOptionCount field value. + * @param tallyPublicKey The sfTallyPublicKey field value. + * @param openTime The sfOpenTime field value. + * @param closeTime The sfCloseTime field value. + * @param zKProof The sfZKProof field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + BallotCreateBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& digest, std::decay_t const& optionCount, std::decay_t const& tallyPublicKey, std::decay_t const& openTime, std::decay_t const& closeTime, std::decay_t const& zKProof, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttBALLOT_CREATE, account, sequence, fee) + { + setDigest(digest); + setOptionCount(optionCount); + setTallyPublicKey(tallyPublicKey); + setOpenTime(openTime); + setCloseTime(closeTime); + setZKProof(zKProof); + } + + /** + * @brief Construct a BallotCreateBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + BallotCreateBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttBALLOT_CREATE) + { + throw std::runtime_error("Invalid transaction type for BallotCreateBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfMPTokenIssuanceID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setMPTokenIssuanceID(std::decay_t const& value) + { + object_[sfMPTokenIssuanceID] = value; + return *this; + } + + /** + * @brief Set sfDomainID (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setDomainID(std::decay_t const& value) + { + object_[sfDomainID] = value; + return *this; + } + + /** + * @brief Set sfDigest (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setDigest(std::decay_t const& value) + { + object_[sfDigest] = value; + return *this; + } + + /** + * @brief Set sfURI (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setURI(std::decay_t const& value) + { + object_[sfURI] = value; + return *this; + } + + /** + * @brief Set sfOptionCount (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setOptionCount(std::decay_t const& value) + { + object_[sfOptionCount] = value; + return *this; + } + + /** + * @brief Set sfTallyPublicKey (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setTallyPublicKey(std::decay_t const& value) + { + object_[sfTallyPublicKey] = value; + return *this; + } + + /** + * @brief Set sfAuditorEncryptionKey (SoeOptional) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setAuditorEncryptionKey(std::decay_t const& value) + { + object_[sfAuditorEncryptionKey] = value; + return *this; + } + + /** + * @brief Set sfOpenTime (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setOpenTime(std::decay_t const& value) + { + object_[sfOpenTime] = value; + return *this; + } + + /** + * @brief Set sfCloseTime (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setCloseTime(std::decay_t const& value) + { + object_[sfCloseTime] = value; + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotCreateBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Build and return the BallotCreate wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + BallotCreate + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return BallotCreate{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/BallotDelete.h b/include/xrpl/protocol_autogen/transactions/BallotDelete.h new file mode 100644 index 00000000000..c2bf3e9d8f3 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/BallotDelete.h @@ -0,0 +1,131 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class BallotDeleteBuilder; + +/** + * @brief Transaction: BallotDelete + * + * Type: ttBALLOT_DELETE (109) + * Delegable: Delegation::NotDelegable + * Amendment: featureConfidentialVoting + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use BallotDeleteBuilder to construct new transactions. + */ +class BallotDelete : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttBALLOT_DELETE; + + /** + * @brief Construct a BallotDelete transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit BallotDelete(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for BallotDelete"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfBallotID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBallotID() const + { + return this->tx_->at(sfBallotID); + } +}; + +/** + * @brief Builder for BallotDelete transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class BallotDeleteBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new BallotDeleteBuilder with required fields. + * @param account The account initiating the transaction. + * @param ballotID The sfBallotID field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + BallotDeleteBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& ballotID, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttBALLOT_DELETE, account, sequence, fee) + { + setBallotID(ballotID); + } + + /** + * @brief Construct a BallotDeleteBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + BallotDeleteBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttBALLOT_DELETE) + { + throw std::runtime_error("Invalid transaction type for BallotDeleteBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfBallotID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotDeleteBuilder& + setBallotID(std::decay_t const& value) + { + object_[sfBallotID] = value; + return *this; + } + + /** + * @brief Build and return the BallotDelete wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + BallotDelete + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return BallotDelete{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/protocol_autogen/transactions/BallotFinalize.h b/include/xrpl/protocol_autogen/transactions/BallotFinalize.h new file mode 100644 index 00000000000..8934908b815 --- /dev/null +++ b/include/xrpl/protocol_autogen/transactions/BallotFinalize.h @@ -0,0 +1,179 @@ +// This file is auto-generated. Do not edit. +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl::transactions { + +class BallotFinalizeBuilder; + +/** + * @brief Transaction: BallotFinalize + * + * Type: ttBALLOT_FINALIZE (108) + * Delegable: Delegation::NotDelegable + * Amendment: featureConfidentialVoting + * Privileges: Privilege::NoPriv + * + * Immutable wrapper around STTx providing type-safe field access. + * Use BallotFinalizeBuilder to construct new transactions. + */ +class BallotFinalize : public TransactionBase +{ +public: + static constexpr xrpl::TxType txType = ttBALLOT_FINALIZE; + + /** + * @brief Construct a BallotFinalize transaction wrapper from an existing STTx object. + * @throws std::runtime_error if the transaction type doesn't match. + */ + explicit BallotFinalize(std::shared_ptr tx) + : TransactionBase(std::move(tx)) + { + // Verify transaction type + if (tx_->getTxnType() != txType) + { + throw std::runtime_error("Invalid transaction type for BallotFinalize"); + } + } + + // Transaction-specific field getters + + /** + * @brief Get sfBallotID (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_UINT256::type::value_type + getBallotID() const + { + return this->tx_->at(sfBallotID); + } + /** + * @brief Get sfResults (SoeRequired) + * @note This is an untyped field. + * @return The field value. + */ + [[nodiscard]] + STArray const& + getResults() const + { + return this->tx_->getFieldArray(sfResults); + } + + /** + * @brief Get sfZKProof (SoeRequired) + * @return The field value. + */ + [[nodiscard]] + SF_VL::type::value_type + getZKProof() const + { + return this->tx_->at(sfZKProof); + } +}; + +/** + * @brief Builder for BallotFinalize transactions. + * + * Provides a fluent interface for constructing transactions with method chaining. + * Uses STObject internally for flexible transaction construction. + * Inherits common field setters from TransactionBuilderBase. + */ +class BallotFinalizeBuilder : public TransactionBuilderBase +{ +public: + /** + * @brief Construct a new BallotFinalizeBuilder with required fields. + * @param account The account initiating the transaction. + * @param ballotID The sfBallotID field value. + * @param results The sfResults field value. + * @param zKProof The sfZKProof field value. + * @param sequence Optional sequence number for the transaction. + * @param fee Optional fee for the transaction. + */ + BallotFinalizeBuilder(SF_ACCOUNT::type::value_type account, + std::decay_t const& ballotID, STArray const& results, std::decay_t const& zKProof, std::optional sequence = std::nullopt, + std::optional fee = std::nullopt +) + : TransactionBuilderBase(ttBALLOT_FINALIZE, account, sequence, fee) + { + setBallotID(ballotID); + setResults(results); + setZKProof(zKProof); + } + + /** + * @brief Construct a BallotFinalizeBuilder from an existing STTx object. + * @param tx The existing transaction to copy from. + * @throws std::runtime_error if the transaction type doesn't match. + */ + BallotFinalizeBuilder(std::shared_ptr tx) + { + if (tx->getTxnType() != ttBALLOT_FINALIZE) + { + throw std::runtime_error("Invalid transaction type for BallotFinalizeBuilder"); + } + object_ = *tx; + } + + /** + * @brief Transaction-specific field setters + */ + + /** + * @brief Set sfBallotID (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotFinalizeBuilder& + setBallotID(std::decay_t const& value) + { + object_[sfBallotID] = value; + return *this; + } + + /** + * @brief Set sfResults (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotFinalizeBuilder& + setResults(STArray const& value) + { + object_.setFieldArray(sfResults, value); + return *this; + } + + /** + * @brief Set sfZKProof (SoeRequired) + * @return Reference to this builder for method chaining. + */ + BallotFinalizeBuilder& + setZKProof(std::decay_t const& value) + { + object_[sfZKProof] = value; + return *this; + } + + /** + * @brief Build and return the BallotFinalize wrapper. + * @param publicKey The public key for signing. + * @param secretKey The secret key for signing. + * @return The constructed transaction wrapper. + */ + BallotFinalize + build(PublicKey const& publicKey, SecretKey const& secretKey) + { + sign(publicKey, secretKey); + return BallotFinalize{std::make_shared(std::move(object_))}; + } +}; + +} // namespace xrpl::transactions diff --git a/include/xrpl/tx/invariants/BallotInvariant.h b/include/xrpl/tx/invariants/BallotInvariant.h new file mode 100644 index 00000000000..016ad1dc3af --- /dev/null +++ b/include/xrpl/tx/invariants/BallotInvariant.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +/** + * @brief Invariants: Confidential ballot consistency. + * + * - The encrypted tally array always has exactly OptionCount entries. + * - A results array, when present, has exactly OptionCount entries and only + * appears on a finalized ballot. + * - A ballot's VoteCount never decreases, and rises by exactly the number of + * BallotVote objects created against it in the same transaction. + * - No BallotVote is created without a matching VoteCount increment on its + * ballot. + */ +class ValidBallot +{ + struct BallotChange + { + std::shared_ptr before; + std::shared_ptr after; + }; + + std::map ballots_; + std::map createdVotes_; + +public: + /** + * @brief Track ballot modifications and BallotVote creations. + * + * @param isDelete Whether the ledger entry is being deleted. + * @param before The ledger entry before transaction application. + * @param after The ledger entry after transaction application. + */ + void + visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after); + + /** + * @brief Verify ballot tally, results, and vote-count invariants. + * + * @param tx The transaction being checked. + * @param result The transaction result code. + * @param fee The fee charged by the transaction. + * @param view The ledger view after transaction application. + * @param j Journal used for diagnostics. + * @return true if the invariant checks pass, otherwise false. + */ + bool + finalize( + STTx const& tx, + TER const result, + XRPAmount const fee, + ReadView const& view, + beast::Journal const& j); +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/invariants/InvariantCheck.h b/include/xrpl/tx/invariants/InvariantCheck.h index e8dafbd3017..4642b91d620 100644 --- a/include/xrpl/tx/invariants/InvariantCheck.h +++ b/include/xrpl/tx/invariants/InvariantCheck.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -463,7 +464,8 @@ using InvariantChecks = std::tuple< ValidMPTTransfer, ObjectHasPseudoAccount, SponsorshipOwnerCountsMatch, - SponsorshipAccountCountMatchesField>; + SponsorshipAccountCountMatchesField, + ValidBallot>; /** * @brief get a tuple of all invariant checks diff --git a/include/xrpl/tx/transactors/voting/BallotCastVote.h b/include/xrpl/tx/transactors/voting/BallotCastVote.h new file mode 100644 index 00000000000..50dbd39d679 --- /dev/null +++ b/include/xrpl/tx/transactors/voting/BallotCastVote.h @@ -0,0 +1,73 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Casts an encrypted, zero-knowledge-proven vote onto a Ballot. + * + * Every cast submits a vector of N ciphertexts (one per option) under the + * tally key: the chosen option(s) encrypt the voter's weight, the rest encrypt + * zero. Validators homomorphically add the vector into the ballot's encrypted + * tally, so the running count stays hidden. The cast proves each option value + * is non-negative (aggregated Bulletproof) and that the vector sums to the + * voter's public weight W. + * + * @note v1 crypto: range and sum-to-W are verified against the merged + * mpt-crypto 0.4.0-rc4 primitives. The per-option ciphertext-commitment + * linkage requires a verifiable-encryption primitive not yet exported by + * the library; credential-mode casts are gated (rejected) on it. See + * plan-2-implementation.md. + * + * @see BallotCreate, BallotFinalize + */ +class BallotCastVote : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit BallotCastVote(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/voting/BallotCreate.h b/include/xrpl/tx/transactors/voting/BallotCreate.h new file mode 100644 index 00000000000..f1577b7f282 --- /dev/null +++ b/include/xrpl/tx/transactors/voting/BallotCreate.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * @brief Creates a confidential Ballot with a homomorphically encrypted tally. + * + * A ballot references exactly one eligibility source: an MPT issuance + * (token-weighted voting) or a permissioned domain (credential-gated, one + * account one vote). Each per-option tally counter is initialized to a + * canonical encryption of zero under the tally authority's ElGamal key, which + * the creator registers with a Schnorr proof of key possession. + * + * @see BallotCastVote, BallotFinalize, BallotDelete + */ +class BallotCreate : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit BallotCreate(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + checkExtraFeatures(PreflightContext const& ctx); + + static std::uint32_t + getFlagsMask(PreflightContext const& ctx); + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/voting/BallotDelete.h b/include/xrpl/tx/transactors/voting/BallotDelete.h new file mode 100644 index 00000000000..d8d55b1eccf --- /dev/null +++ b/include/xrpl/tx/transactors/voting/BallotDelete.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Deletes a Ballot or a BallotVote to reclaim its owner reserve. + * + * The ballot creator may delete a finalized ballot, or an unfinalized one past + * its close time. A voter may delete their own BallotVote any time after the + * ballot closes. BallotVote objects are independent of the Ballot's lifetime, + * so deleting the ballot never orphans a voter's reserve. + * + * @see BallotCreate, BallotCastVote, BallotFinalize + */ +class BallotDelete : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit BallotDelete(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/include/xrpl/tx/transactors/voting/BallotFinalize.h b/include/xrpl/tx/transactors/voting/BallotFinalize.h new file mode 100644 index 00000000000..bd1b26cc104 --- /dev/null +++ b/include/xrpl/tx/transactors/voting/BallotFinalize.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace xrpl { + +/** + * @brief Publishes the verifiable plaintext results of a closed Ballot. + * + * After the voting window closes, the tally authority submits the claimed + * per-option counts together with a per-option decryption-correctness proof + * (a discrete-log-equality proof against the on-ledger tally ciphertexts under + * the tally key). Validators reject any counts that do not match the ciphertexts, + * so the result is trustless even though the tallying is not. + * + * @see BallotCreate, BallotCastVote + */ +class BallotFinalize : public Transactor +{ +public: + static constexpr auto kConsequencesFactory = ConsequencesFactoryType::Normal; + + explicit BallotFinalize(ApplyContext& ctx) : Transactor(ctx) + { + } + + static NotTEC + preflight(PreflightContext const& ctx); + + static XRPAmount + calculateBaseFee(ReadView const& view, STTx const& tx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; + + void + visitInvariantEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) override; + + [[nodiscard]] bool + finalizeInvariants( + STTx const& tx, + TER result, + XRPAmount fee, + ReadView const& view, + beast::Journal const& j) override; +}; + +} // namespace xrpl diff --git a/src/libxrpl/ledger/helpers/TokenHelpers.cpp b/src/libxrpl/ledger/helpers/TokenHelpers.cpp index 2c2c943a9fa..7c1b7535313 100644 --- a/src/libxrpl/ledger/helpers/TokenHelpers.cpp +++ b/src/libxrpl/ledger/helpers/TokenHelpers.cpp @@ -405,6 +405,29 @@ accountHolds( view, account, issue.currency, issue.account, zeroIfFrozen, j, includeFullBalance); } +// ConfidentialVoting: amount locked on an MPToken while a ballot it voted on is +// still open. The lock keys off the ballot's CloseTime, so it releases +// automatically once the voting window ends, with no cleanup transaction. +static std::uint64_t +activeVoteLock(ReadView const& view, std::shared_ptr const& sleMpt) +{ + if (!view.rules().enabled(featureConfidentialVoting) || + !sleMpt->isFieldPresent(sfVoteLockedAmount) || !sleMpt->isFieldPresent(sfBallotID)) + { + return 0; + } + + auto const sleBallot = view.read(keylet::ballot((*sleMpt)[sfBallotID])); + if (!sleBallot) + return 0; // ballot deleted; lock no longer applies + + auto const closeTime = (*sleBallot)[sfCloseTime]; + if (view.parentCloseTime().time_since_epoch().count() >= closeTime) + return 0; // window closed; lock released + + return (*sleMpt)[sfVoteLockedAmount]; +} + STAmount accountHolds( ReadView const& view, @@ -467,6 +490,20 @@ accountHolds( !sleMpt->isFlag(lsfMPTAuthorized)) amount.clear(mptIssue); } + + // Reduce spendable balance by any active vote lock (token-mode ballots). + // FullBalance requests (e.g. accounting/display) see the raw balance. + if (includeFullBalance != SpendableHandling::FullBalance) + { + if (auto const locked = activeVoteLock(view, sleMpt); locked > 0) + { + auto const cur = amount.mpt().value(); // std::int64_t + auto const lockedSigned = static_cast(locked); + amount = STAmount{ + mptIssue, + static_cast(cur > lockedSigned ? cur - lockedSigned : 0)}; + } + } } if (view.rules().enabled(featureMPTokensV2)) diff --git a/src/libxrpl/protocol/ConfidentialTransfer.cpp b/src/libxrpl/protocol/ConfidentialTransfer.cpp index ecd48329288..acbf2cd1d2d 100644 --- a/src/libxrpl/protocol/ConfidentialTransfer.cpp +++ b/src/libxrpl/protocol/ConfidentialTransfer.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,7 @@ #include #include +#include #include #include #include @@ -550,4 +552,118 @@ verifyConvertBackProof( return tesSUCCESS; } +uint256 +getBallotCastContextHash(AccountID const& account, uint256 const& ballotID, std::uint32_t sequence) +{ + // Domain-separated from other confidential context hashes by a distinct tag. + return sha512Half(std::uint16_t(0x4243) /* 'BC' */, account, ballotID, sequence); +} + +uint256 +getBallotFinalizeContextHash( + AccountID const& account, + uint256 const& ballotID, + std::uint32_t sequence) +{ + return sha512Half(std::uint16_t(0x4246) /* 'BF' */, account, ballotID, sequence); +} + +TER +verifyBallotRangeProof( + Slice const& proof, + std::vector const& commitments, + uint256 const& contextHash) +{ + std::vector ptrs; + ptrs.reserve(commitments.size()); + for (auto const& c : commitments) + { + if (c.size() != kEcPedersenCommitmentLength) + return tecINTERNAL; // LCOV_EXCL_LINE + ptrs.push_back(c.data()); + } + + if (mpt_verify_aggregated_bulletproof( + proof.data(), proof.size(), ptrs.data(), ptrs.size(), contextHash.data()) != 0) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + +TER +verifyBallotVoteLinkage( + std::vector const& pubKeys, + Slice const& c1, + std::vector const& c2PerKey, + Slice const& commitment, + Slice const& proof, + uint256 const& contextHash) +{ + auto const n = pubKeys.size(); + if (n == 0 || n > 3 || c2PerKey.size() != n) + return tecINTERNAL; // LCOV_EXCL_LINE + if (proof.size() != kEcSendSigmaProofLength) + return tecBAD_PROOF; + if (c1.size() != kEcCiphertextComponentLength || + commitment.size() != kEcPedersenCommitmentLength) + return tecBAD_PROOF; + + auto* const ctx = secp256k1Context(); + + secp256k1_pubkey c1Pt; + if (secp256k1_ec_pubkey_parse(ctx, &c1Pt, c1.data(), c1.size()) != 1) + return tecBAD_PROOF; + + std::vector c2Pts(n); + std::vector pkPts(n); + for (std::size_t i = 0; i < n; ++i) + { + if (c2PerKey[i].size() != kEcCiphertextComponentLength || + pubKeys[i].size() != kEcPubKeyLength) + return tecBAD_PROOF; + if (secp256k1_ec_pubkey_parse(ctx, &c2Pts[i], c2PerKey[i].data(), c2PerKey[i].size()) != 1) + return tecBAD_PROOF; + if (secp256k1_ec_pubkey_parse(ctx, &pkPts[i], pubKeys[i].data(), pubKeys[i].size()) != 1) + return tecBAD_PROOF; + } + + secp256k1_pubkey pcm; + if (secp256k1_ec_pubkey_parse(ctx, &pcm, commitment.data(), commitment.size()) != 1) + return tecBAD_PROOF; + + // Canonical vacuous balance witness: sk_A = 1 makes pk_A = B1 = B2 = G, and + // rho_b = 1 with b = 0 makes PC_b = H. Both sides derive these identically, + // so nothing balance-related is carried on the wire and the terms constrain + // nothing about the vote. + std::array one{}; + one[kEcScalarLength - 1] = 1; + secp256k1_pubkey pkA; + if (secp256k1_ec_pubkey_create(ctx, &pkA, one.data()) != 1) + return tecINTERNAL; // LCOV_EXCL_LINE + secp256k1_pubkey pcb; + if (secp256k1_mpt_get_h_generator(ctx, &pcb) != 1) + return tecINTERNAL; // LCOV_EXCL_LINE + + if (secp256k1_compact_standard_verify( + ctx, + proof.data(), + n, + &c1Pt, + c2Pts.data(), + pkPts.data(), + &pcm, + &pkA, + &pcb, + &pkA, // B1 = G + &pkA, // B2 = G + contextHash.data()) != 1) + { + return tecBAD_PROOF; + } + + return tesSUCCESS; +} + } // namespace xrpl diff --git a/src/libxrpl/protocol/Indexes.cpp b/src/libxrpl/protocol/Indexes.cpp index 91ed5c893fd..1251ff9d80d 100644 --- a/src/libxrpl/protocol/Indexes.cpp +++ b/src/libxrpl/protocol/Indexes.cpp @@ -104,6 +104,8 @@ enum class LedgerNameSpace : std::uint16_t { LoanBroker = 'l', // lower-case L Loan = 'L', Sponsorship = '>', + Ballot = 'b', + BallotVote = 'v', // No longer used or supported. Left here to reserve the space to avoid accidental reuse. Contract [[deprecated]] = 'c', @@ -610,6 +612,18 @@ permissionedDomain(uint256 const& domainID) noexcept return {ltPERMISSIONED_DOMAIN, domainID}; } +Keylet +ballot(AccountID const& owner, std::uint32_t seq) noexcept +{ + return {ltBALLOT, indexHash(LedgerNameSpace::Ballot, owner, seq)}; +} + +Keylet +ballotVote(uint256 const& ballotID, AccountID const& voter) noexcept +{ + return {ltBALLOT_VOTE, indexHash(LedgerNameSpace::BallotVote, ballotID, voter)}; +} + } // namespace keylet } // namespace xrpl diff --git a/src/libxrpl/protocol/InnerObjectFormats.cpp b/src/libxrpl/protocol/InnerObjectFormats.cpp index 5cb7d166e9f..c3ed7ab5c40 100644 --- a/src/libxrpl/protocol/InnerObjectFormats.cpp +++ b/src/libxrpl/protocol/InnerObjectFormats.cpp @@ -168,6 +168,24 @@ InnerObjectFormats::InnerObjectFormats() {sfTxnSignature, SoeOptional}, {sfSigners, SoeOptional}, }); + + // One entry in a confidential ballot ciphertext vector: an ElGamal + // ciphertext, plus (on cast transactions) the matching Pedersen + // commitment used for the aggregated range proof. + add(sfBallotOption.jsonName, + sfBallotOption.getCode(), + { + {sfEncryptedVote, SoeRequired}, + {sfAmountCommitment, SoeOptional}, + {sfZKProof, SoeOptional}, + }); + + // One entry in a finalized ballot's plaintext result vector. + add(sfBallotResult.jsonName, + sfBallotResult.getCode(), + { + {sfBallotWeight, SoeRequired}, + }); } InnerObjectFormats const& diff --git a/src/libxrpl/protocol/TER.cpp b/src/libxrpl/protocol/TER.cpp index c6ebe986424..54456391dbf 100644 --- a/src/libxrpl/protocol/TER.cpp +++ b/src/libxrpl/protocol/TER.cpp @@ -110,6 +110,11 @@ transResults() MAKE_ERROR(tecNO_SPONSOR_PERMISSION, "Sponsor has not authorized this transaction."), MAKE_ERROR(tecOUT_OF_GAS, "The WASM code ran out of gas during execution."), MAKE_ERROR(tecBYTECODE_REJECTED, "The custom WASM code that was run rejected your transaction."), + MAKE_ERROR(tecBALLOT_CLOSED, "The ballot voting window has closed."), + MAKE_ERROR(tecBALLOT_VOTED, "The account has already voted on this ballot."), + MAKE_ERROR(tecBALLOT_NOT_OPEN, "The ballot voting window has not opened yet."), + MAKE_ERROR(tecBALLOT_EXISTS, "The issuance already has an open ballot."), + MAKE_ERROR(tecBALLOT_BAD_OPTIONS, "The vector does not match the ballot's option count."), MAKE_ERROR(tefALREADY, "The exact transaction was already in this ledger."), MAKE_ERROR(tefBAD_ADD_AUTH, "Not authorized to add account."), diff --git a/src/libxrpl/tx/invariants/BallotInvariant.cpp b/src/libxrpl/tx/invariants/BallotInvariant.cpp new file mode 100644 index 00000000000..a1cd642c024 --- /dev/null +++ b/src/libxrpl/tx/invariants/BallotInvariant.cpp @@ -0,0 +1,94 @@ +#include + +#include +#include +#include +#include + +#include + +namespace xrpl { + +void +ValidBallot::visitEntry( + bool isDelete, + std::shared_ptr const& before, + std::shared_ptr const& after) +{ + if (!isDelete && after && after->getType() == ltBALLOT) + { + auto& change = ballots_[after->key()]; + change.before = before; + change.after = after; + } + + if (!isDelete && !before && after && after->getType() == ltBALLOT_VOTE) + createdVotes_[(*after)[sfBallotID]] += 1; +} + +bool +ValidBallot::finalize( + STTx const&, + TER const, + XRPAmount const, + ReadView const&, + beast::Journal const& j) +{ + for (auto const& [key, change] : ballots_) + { + auto const& b = change.after; + std::uint32_t const optionCount = (*b)[sfOptionCount]; + + if (!b->isFieldPresent(sfEncryptedTally) || + b->getFieldArray(sfEncryptedTally).size() != optionCount) + { + JLOG(j.fatal()) << "Invariant failed: ballot tally length != OptionCount"; + return false; + } + + if (b->isFieldPresent(sfResults)) + { + if (!b->isFlag(lsfBallotFinalized)) + { + JLOG(j.fatal()) << "Invariant failed: ballot results present without finalize flag"; + return false; + } + if (b->getFieldArray(sfResults).size() != optionCount) + { + JLOG(j.fatal()) << "Invariant failed: ballot results length != OptionCount"; + return false; + } + } + + std::uint32_t const afterVotes = (*b)[sfVoteCount]; + std::uint32_t const beforeVotes = change.before ? (*change.before)[sfVoteCount] : 0u; + if (afterVotes < beforeVotes) + { + JLOG(j.fatal()) << "Invariant failed: ballot VoteCount decreased"; + return false; + } + + auto const it = createdVotes_.find(key); + std::uint32_t const created = it == createdVotes_.end() ? 0u : it->second; + if (afterVotes - beforeVotes != created) + { + JLOG(j.fatal()) << "Invariant failed: ballot VoteCount delta != BallotVotes created"; + return false; + } + } + + // Every created BallotVote must correspond to a VoteCount increment on a + // ballot modified in the same transaction. + for (auto const& [ballotID, count] : createdVotes_) + { + if (!ballots_.contains(ballotID)) + { + JLOG(j.fatal()) << "Invariant failed: BallotVote created without ballot update"; + return false; + } + } + + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/voting/BallotCastVote.cpp b/src/libxrpl/tx/transactors/voting/BallotCastVote.cpp new file mode 100644 index 00000000000..b4c91b5b83a --- /dev/null +++ b/src/libxrpl/tx/transactors/voting/BallotCastVote.cpp @@ -0,0 +1,483 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace xrpl { + +namespace { + +// Every ciphertext entry in a cast is a 66-byte ElGamal ciphertext; entries in +// the primary vote vector additionally carry a 33-byte Pedersen commitment. +bool +validCiphertextArray(STArray const& arr, bool requireCommitment) +{ + for (auto const& entry : arr) + { + if (!entry.isFieldPresent(sfEncryptedVote) || + entry[sfEncryptedVote].length() != kEcGamalEncryptedTotalLength || + !isValidCiphertext(entry[sfEncryptedVote])) + { + return false; + } + + bool const hasCommitment = entry.isFieldPresent(sfAmountCommitment); + if (requireCommitment != hasCommitment) + return false; + + if (hasCommitment && !isValidCompressedECPoint(entry[sfAmountCommitment])) + return false; + + // The primary vector also carries a per-option linkage proof binding the + // ciphertext to its range-proven commitment. + if (requireCommitment) + { + if (!entry.isFieldPresent(sfZKProof) || + entry[sfZKProof].length() != kEcSendSigmaProofLength) + return false; + } + } + return true; +} + +// Homomorphic sum of the ciphertexts in a vote vector. +std::optional +sumCiphertexts(STArray const& arr) +{ + std::optional acc; + for (auto const& entry : arr) + { + Slice const ct = entry[sfEncryptedVote]; + if (!acc) + { + acc.emplace(ct.data(), ct.size()); + continue; + } + acc = homomorphicAdd(*acc, ct); + if (!acc) + return std::nullopt; + } + return acc; +} + +// Verify the homomorphic sum of a vote vector equals Enc(weight; R) under key. +// Revealing the aggregate randomness R leaks nothing (weight is public) but +// pins the vector's total to the voter's weight, blocking weight inflation. +TER +verifySumToWeight( + STArray const& votes, + Slice const& pubKey, + std::uint64_t weight, + Slice const& blinding) +{ + auto const sum = sumCiphertexts(votes); + if (!sum) + return tecBAD_PROOF; + + auto const expected = encryptAmount(weight, pubKey, blinding); + if (!expected) + return tecBAD_PROOF; + + if (sum->size() != expected->size() || + std::memcmp(sum->data(), expected->data(), sum->size()) != 0) + { + return tecBAD_PROOF; + } + return tesSUCCESS; +} + +// Copy a cast vote vector into a lean stored form (ciphertext only). +STArray +storedVoteVector(STArray const& src) +{ + STArray out; + for (auto const& entry : src) + { + STObject o = STObject::makeInnerObject(sfBallotOption); + o.setFieldVL(sfEncryptedVote, entry[sfEncryptedVote]); + out.push_back(std::move(o)); + } + return out; +} + +} // namespace + +bool +BallotCastVote::checkExtraFeatures(PreflightContext const& ctx) +{ + return !ctx.tx.isFieldPresent(sfCredentialIDs) || ctx.rules.enabled(featureCredentials); +} + +NotTEC +BallotCastVote::preflight(PreflightContext const& ctx) +{ + auto const& votes = ctx.tx.getFieldArray(sfEncryptedVotes); + if (votes.empty() || votes.size() > 8) + return temMALFORMED; + + // Primary vote vector carries ciphertext + range-proof commitment per option. + if (!validCiphertextArray(votes, /*requireCommitment=*/true)) + return temBAD_CIPHERTEXT; + + bool const hasAuditor = ctx.tx.isFieldPresent(sfAuditorEncryptedVotes); + if (hasAuditor) + { + auto const& av = ctx.tx.getFieldArray(sfAuditorEncryptedVotes); + if (av.size() != votes.size() || !validCiphertextArray(av, /*requireCommitment=*/false)) + return temBAD_CIPHERTEXT; + } + + bool const hasVoterKey = ctx.tx.isFieldPresent(sfVoterPublicKey); + bool const hasVoterVotes = ctx.tx.isFieldPresent(sfVoterEncryptedVotes); + if (hasVoterKey != hasVoterVotes) + return temMALFORMED; + + if (hasVoterKey) + { + if (!isValidCompressedECPoint(ctx.tx[sfVoterPublicKey])) + return temMALFORMED; + + auto const& vv = ctx.tx.getFieldArray(sfVoterEncryptedVotes); + if (vv.size() != votes.size() || !validCiphertextArray(vv, /*requireCommitment=*/false)) + return temBAD_CIPHERTEXT; + } + + if (ctx.tx[sfZKProof].empty()) + return temMALFORMED; + + if (auto const err = credentials::checkFields(ctx.tx, ctx.rules, ctx.j); !isTesSuccess(err)) + return err; + + return tesSUCCESS; +} + +XRPAmount +BallotCastVote::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +BallotCastVote::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + auto const ballotID = ctx.tx[sfBallotID]; + auto const sleBallot = ctx.view.read(keylet::ballot(ballotID)); + if (!sleBallot) + return tecNO_ENTRY; + + if (sleBallot->isFlag(lsfBallotFinalized)) + return tecBALLOT_CLOSED; + + // Voting window: [OpenTime, CloseTime). + auto const now = ctx.view.parentCloseTime().time_since_epoch().count(); + if (now < (*sleBallot)[sfOpenTime]) + return tecBALLOT_NOT_OPEN; + if (now >= (*sleBallot)[sfCloseTime]) + return tecBALLOT_CLOSED; + + // Vote vectors must have exactly one entry per option. + std::uint8_t const n = (*sleBallot)[sfOptionCount]; + auto const& votes = ctx.tx.getFieldArray(sfEncryptedVotes); + if (votes.size() != n) + return tecBALLOT_BAD_OPTIONS; + + // Auditor / voter-recovery presence must match the ballot's configuration. + bool const ballotHasAuditor = sleBallot->isFieldPresent(sfAuditorEncryptionKey); + if (ballotHasAuditor != ctx.tx.isFieldPresent(sfAuditorEncryptedVotes)) + return tecNO_PERMISSION; + + bool const ballotRecoverable = sleBallot->isFlag(lsfVoterRecoverable); + if (ballotRecoverable != ctx.tx.isFieldPresent(sfVoterPublicKey)) + return tecNO_PERMISSION; + + // One BallotVote per account per ballot. + if (ctx.view.exists(keylet::ballotVote(ballotID, account))) + return tecBALLOT_VOTED; + + bool const tokenMode = sleBallot->isFieldPresent(sfMPTokenIssuanceID); + + // Eligibility and weight. + std::uint64_t weight = 0; + if (tokenMode) + { + MPTIssue const mptIssue{(*sleBallot)[sfMPTokenIssuanceID]}; + if (!ctx.view.exists(keylet::mptoken(mptIssue.getMptID(), account))) + return tecNO_ENTRY; + + auto const held = accountHolds( + ctx.view, + account, + mptIssue, + FreezeHandling::ZeroIfFrozen, + AuthHandling::ZeroIfUnauthorized, + ctx.j); + if (held.mpt().value() <= 0) + return tecNO_ENTRY; + weight = static_cast(held.mpt().value()); + } + else + { + if (!permissioned_dex::accountInDomain(ctx.view, account, (*sleBallot)[sfDomainID])) + return tecNO_PERMISSION; + weight = 1; + } + + // --- Cast proof verification ------------------------------------------- + auto const ctxHash = getBallotCastContextHash(account, ballotID, ctx.tx.getSeqProxy().value()); + auto const blindingFactor = ctx.tx[sfBlindingFactor]; + Slice const blinding{blindingFactor.data(), blindingFactor.size()}; + + // 1. Range: each committed option value is non-negative. + std::vector commitments; + commitments.reserve(votes.size()); + for (auto const& entry : votes) + commitments.push_back(entry[sfAmountCommitment]); + if (auto const ter = verifyBallotRangeProof(ctx.tx[sfZKProof], commitments, ctxHash); + !isTesSuccess(ter)) + return ter; + + // 2. Sum-to-weight against the tally key. + if (auto const ter = verifySumToWeight(votes, (*sleBallot)[sfTallyPublicKey], weight, blinding); + !isTesSuccess(ter)) + return ter; + + // 3. Mirror consistency: auditor / voter vectors carry the same aggregate. + if (ballotHasAuditor) + { + if (auto const ter = verifySumToWeight( + ctx.tx.getFieldArray(sfAuditorEncryptedVotes), + (*sleBallot)[sfAuditorEncryptionKey], + weight, + blinding); + !isTesSuccess(ter)) + return ter; + } + if (ballotRecoverable) + { + if (auto const ter = verifySumToWeight( + ctx.tx.getFieldArray(sfVoterEncryptedVotes), + ctx.tx[sfVoterPublicKey], + weight, + blinding); + !isTesSuccess(ter)) + return ter; + } + + // 4. Per-option ciphertext-commitment linkage. Without it a voter could put + // (W+k, -k) in the ciphertexts while committing (W, 0): sum and range + // both pass, but the tally would be corrupted. Each option's proof shows + // its ciphertext (and any auditor/voter mirror, under shared randomness) + // encrypts exactly the range-proven committed value. Sound for both token + // and credential mode. + Slice const tallyKey = (*sleBallot)[sfTallyPublicKey]; + STArray const* auditorVotes = + ballotHasAuditor ? &ctx.tx.getFieldArray(sfAuditorEncryptedVotes) : nullptr; + STArray const* voterVotes = + ballotRecoverable ? &ctx.tx.getFieldArray(sfVoterEncryptedVotes) : nullptr; + + for (std::uint8_t i = 0; i < n; ++i) + { + Slice const tallyCt = votes[i][sfEncryptedVote]; + Slice const c1{tallyCt.data(), kEcCiphertextComponentLength}; + + std::vector mirrorKeys{tallyKey}; + std::vector c2PerKey{ + Slice{tallyCt.data() + kEcCiphertextComponentLength, kEcCiphertextComponentLength}}; + + // Auditor / voter mirrors must share the option's ElGamal randomness, so + // their C1 must equal the tally C1; only then does one shared-C1 linkage + // proof pin every mirror to the same value. + auto const addMirror = [&](STArray const* arr, Slice const& key) -> bool { + Slice const ct = (*arr)[i][sfEncryptedVote]; + if (std::memcmp(ct.data(), c1.data(), kEcCiphertextComponentLength) != 0) + return false; + mirrorKeys.push_back(key); + c2PerKey.push_back( + Slice{ct.data() + kEcCiphertextComponentLength, kEcCiphertextComponentLength}); + return true; + }; + if (auditorVotes && !addMirror(auditorVotes, (*sleBallot)[sfAuditorEncryptionKey])) + return tecBAD_PROOF; + if (voterVotes && !addMirror(voterVotes, ctx.tx[sfVoterPublicKey])) + return tecBAD_PROOF; + + if (auto const ter = verifyBallotVoteLinkage( + mirrorKeys, + c1, + c2PerKey, + votes[i][sfAmountCommitment], + votes[i][sfZKProof], + ctxHash); + !isTesSuccess(ter)) + { + return ter; + } + } + + return tesSUCCESS; +} + +TER +BallotCastVote::doApply() +{ + auto const ballotID = ctx_.tx[sfBallotID]; + auto sleBallot = view().peek(keylet::ballot(ballotID)); + if (!sleBallot) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const sleOwner = view().peek(keylet::account(accountID_)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + Slice const tallyKey = (*sleBallot)[sfTallyPublicKey]; + auto const blindingFactor = ctx_.tx[sfBlindingFactor]; + Slice const blinding{blindingFactor.data(), blindingFactor.size()}; + auto const& votes = ctx_.tx.getFieldArray(sfEncryptedVotes); + STArray const& oldTally = sleBallot->getFieldArray(sfEncryptedTally); + + if (votes.size() != oldTally.size()) + return tecINTERNAL; // LCOV_EXCL_LINE + + // Homomorphically add each (re-randomized) vote ciphertext into its tally + // counter. Re-randomizing with a public deterministic scalar keeps the + // ledger reproducible while adding Enc(0), so the plaintext tally is + // unchanged. + STArray newTally; + for (std::size_t i = 0; i < oldTally.size(); ++i) + { + auto rr = rerandomizeCiphertext(votes[i][sfEncryptedVote], tallyKey, blinding); + if (!rr) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto sum = homomorphicAdd(oldTally[i][sfEncryptedVote], *rr); + if (!sum) + return tecINTERNAL; // LCOV_EXCL_LINE + + STObject entry = STObject::makeInnerObject(sfBallotOption); + entry.setFieldVL(sfEncryptedVote, *sum); + newTally.push_back(std::move(entry)); + } + sleBallot->setFieldArray(sfEncryptedTally, newTally); + (*sleBallot)[sfVoteCount] = (*sleBallot)[sfVoteCount] + 1u; + + // Recompute the public weight (mirrors preclaim; the lock is not yet set). + bool const tokenMode = sleBallot->isFieldPresent(sfMPTokenIssuanceID); + std::uint64_t weight = 1; + std::optional mptIssue; + if (tokenMode) + { + mptIssue.emplace((*sleBallot)[sfMPTokenIssuanceID]); + auto const held = accountHolds( + view(), + accountID_, + *mptIssue, + FreezeHandling::ZeroIfFrozen, + AuthHandling::ZeroIfUnauthorized, + j_); + weight = static_cast(held.mpt().value()); + } + + // Create the BallotVote object. + auto const voteKeylet = keylet::ballotVote(ballotID, accountID_); + auto sleVote = std::make_shared(voteKeylet); + (*sleVote)[sfAccount] = accountID_; + (*sleVote)[sfBallotID] = ballotID; + (*sleVote)[sfBallotWeight] = weight; + sleVote->setFieldArray(sfEncryptedVotes, storedVoteVector(votes)); + if (ctx_.tx.isFieldPresent(sfAuditorEncryptedVotes)) + { + sleVote->setFieldArray( + sfAuditorEncryptedVotes, + storedVoteVector(ctx_.tx.getFieldArray(sfAuditorEncryptedVotes))); + } + if (ctx_.tx.isFieldPresent(sfVoterPublicKey)) + { + (*sleVote)[sfVoterPublicKey] = ctx_.tx[sfVoterPublicKey]; + sleVote->setFieldArray( + sfVoterEncryptedVotes, storedVoteVector(ctx_.tx.getFieldArray(sfVoterEncryptedVotes))); + } + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; + + auto const page = + view().dirInsert(keylet::ownerDir(accountID_), voteKeylet, describeOwnerDir(accountID_)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*sleVote)[sfOwnerNode] = *page; + + increaseOwnerCount(ctx_.getApplyViewContext(), sleOwner, 1, j_); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleVote); + view().insert(sleVote); + + // Token mode: lock the voter's balance until the ballot closes. + if (tokenMode) + { + auto sleMpt = view().peek(keylet::mptoken(mptIssue->getMptID(), accountID_)); + if (!sleMpt) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleMpt)[sfVoteLockedAmount] = weight; + (*sleMpt)[sfBallotID] = ballotID; + view().update(sleMpt); + } + + view().update(sleBallot); + return tesSUCCESS; +} + +void +BallotCastVote::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +BallotCastVote::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/voting/BallotCreate.cpp b/src/libxrpl/tx/transactors/voting/BallotCreate.cpp new file mode 100644 index 00000000000..6c1c2c2eea9 --- /dev/null +++ b/src/libxrpl/tx/transactors/voting/BallotCreate.cpp @@ -0,0 +1,253 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +// Ballots allow between 2 and 8 options, inclusive. +static constexpr std::uint8_t kMinBallotOptions = 2; +static constexpr std::uint8_t kMaxBallotOptions = 8; +static constexpr std::size_t kMaxBallotUriLength = 256; + +bool +BallotCreate::checkExtraFeatures(PreflightContext const& ctx) +{ + // Credential mode needs the permissioned-domain and credential stacks; + // the base amendment is gated by the TRANSACTION() macro. + if (ctx.tx.isFieldPresent(sfDomainID)) + return ctx.rules.enabled(featurePermissionedDomains) && + ctx.rules.enabled(featureCredentials); + return true; +} + +std::uint32_t +BallotCreate::getFlagsMask(PreflightContext const&) +{ + return tfBallotCreateMask; +} + +NotTEC +BallotCreate::preflight(PreflightContext const& ctx) +{ + // Exactly one eligibility source must be present. + bool const hasIssuance = ctx.tx.isFieldPresent(sfMPTokenIssuanceID); + bool const hasDomain = ctx.tx.isFieldPresent(sfDomainID); + if (hasIssuance == hasDomain) + return temMALFORMED; + + if (hasDomain && ctx.tx[sfDomainID] == beast::kZero) + return temMALFORMED; + + auto const optionCount = ctx.tx[sfOptionCount]; + if (optionCount < kMinBallotOptions || optionCount > kMaxBallotOptions) + return temINVALID_COUNT; + + // Voting window must be non-empty. + if (ctx.tx[sfOpenTime] >= ctx.tx[sfCloseTime]) + return temMALFORMED; + + // The tally key is an ElGamal public key registered with a Schnorr proof. + if (!isValidCompressedECPoint(ctx.tx[sfTallyPublicKey])) + return temMALFORMED; + + if (ctx.tx[sfZKProof].size() != kEcSchnorrProofLength) + return temMALFORMED; + + if (ctx.tx.isFieldPresent(sfAuditorEncryptionKey) && + !isValidCompressedECPoint(ctx.tx[sfAuditorEncryptionKey])) + { + return temMALFORMED; + } + + if (auto const uri = ctx.tx[~sfURI]; uri && (uri->empty() || uri->size() > kMaxBallotUriLength)) + return temMALFORMED; + + return tesSUCCESS; +} + +XRPAmount +BallotCreate::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +BallotCreate::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + // The Schnorr proof of possession is bound to the ballot's own index. + auto const ballotKeylet = keylet::ballot(account, ctx.tx.getSeqProxy().value()); + if (auto const ter = + verifySchnorrProof(ctx.tx[sfTallyPublicKey], ctx.tx[sfZKProof], ballotKeylet.key); + !isTesSuccess(ter)) + return ter; + + if (ctx.tx.isFieldPresent(sfMPTokenIssuanceID)) + { + auto const sleIssuance = + ctx.view.read(keylet::mptokenIssuance(ctx.tx[sfMPTokenIssuanceID])); + if (!sleIssuance) + return tecOBJECT_NOT_FOUND; + + // Only the issuer (in this v1) may create a token-mode ballot. + if (sleIssuance->getAccountID(sfIssuer) != account) + return tecNO_PERMISSION; + + // At most one open ballot per issuance, so a single vote-lock field on + // each MPToken is sufficient. A stale marker for a closed ballot is + // overwritten. + if (auto const openID = (*sleIssuance)[~sfBallotID]) + { + if (auto const sleOpen = ctx.view.read(keylet::ballot(*openID)); sleOpen && + ctx.view.parentCloseTime().time_since_epoch().count() < (*sleOpen)[sfCloseTime]) + { + return tecBALLOT_EXISTS; + } + } + } + else + { + auto const slePD = ctx.view.read(keylet::permissionedDomain(ctx.tx[sfDomainID])); + if (!slePD) + return tecOBJECT_NOT_FOUND; + + // Only the domain owner (in this v1) may create a credential-mode ballot. + if (slePD->getAccountID(sfOwner) != account) + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +BallotCreate::doApply() +{ + auto const sleOwner = view().peek(keylet::account(accountID_)); + if (!sleOwner) + return tefINTERNAL; // LCOV_EXCL_LINE + + if (auto const ret = checkReserve( + ctx_.getApplyViewContext(), sleOwner, preFeeBalance_, {.ownerCountDelta = 1}, j_); + !isTesSuccess(ret)) + return ret; + + auto const seq = ctx_.tx.getSeqProxy().value(); + auto const ballotKeylet = keylet::ballot(accountID_, seq); + auto sleBallot = std::make_shared(ballotKeylet); + + (*sleBallot)[sfOwner] = accountID_; + (*sleBallot)[sfSequence] = seq; + (*sleBallot)[sfDigest] = ctx_.tx[sfDigest]; + (*sleBallot)[sfOptionCount] = ctx_.tx[sfOptionCount]; + (*sleBallot)[sfTallyPublicKey] = ctx_.tx[sfTallyPublicKey]; + (*sleBallot)[sfOpenTime] = ctx_.tx[sfOpenTime]; + (*sleBallot)[sfCloseTime] = ctx_.tx[sfCloseTime]; + (*sleBallot)[sfVoteCount] = 0u; + + std::uint32_t flags = 0; + if (ctx_.tx.isFlag(tfVoterRecoverable)) + flags |= lsfVoterRecoverable; + (*sleBallot)[sfFlags] = flags; + + if (ctx_.tx.isFieldPresent(sfMPTokenIssuanceID)) + (*sleBallot)[sfMPTokenIssuanceID] = ctx_.tx[sfMPTokenIssuanceID]; + else + (*sleBallot)[sfDomainID] = ctx_.tx[sfDomainID]; + + if (ctx_.tx.isFieldPresent(sfURI)) + (*sleBallot)[sfURI] = ctx_.tx[sfURI]; + + if (ctx_.tx.isFieldPresent(sfAuditorEncryptionKey)) + (*sleBallot)[sfAuditorEncryptionKey] = ctx_.tx[sfAuditorEncryptionKey]; + + // Initialize each per-option counter to a deterministic Enc(0) under the + // tally key. The blinding is derived from the ballot index and option so + // every validator computes the identical genesis ciphertext, yet it is a + // valid (non-identity) EC point — Enc(0; 0) would be the point at infinity + // and fail serialization. It decrypts to 0 regardless of the randomness. + STArray tally; + for (std::uint8_t i = 0; i < ctx_.tx[sfOptionCount]; ++i) + { + auto const blinding = sha512Half(ballotKeylet.key, i); + auto const zero = + encryptAmount(0, ctx_.tx[sfTallyPublicKey], Slice(blinding.data(), blinding.size())); + if (!zero) + return tecINTERNAL; // LCOV_EXCL_LINE + + STObject entry = STObject::makeInnerObject(sfBallotOption); + entry.setFieldVL(sfEncryptedVote, *zero); + tally.push_back(std::move(entry)); + } + sleBallot->setFieldArray(sfEncryptedTally, tally); + + auto const page = + view().dirInsert(keylet::ownerDir(accountID_), ballotKeylet, describeOwnerDir(accountID_)); + if (!page) + return tecDIR_FULL; // LCOV_EXCL_LINE + (*sleBallot)[sfOwnerNode] = *page; + + increaseOwnerCount(ctx_.getApplyViewContext(), sleOwner, 1, j_); + addSponsorToLedgerEntry(ctx_.getApplyViewContext(), sleBallot); + + view().insert(sleBallot); + + // Record the open ballot on the issuance so a second one is rejected. + if (ctx_.tx.isFieldPresent(sfMPTokenIssuanceID)) + { + auto sleIssuance = view().peek(keylet::mptokenIssuance(ctx_.tx[sfMPTokenIssuanceID])); + if (!sleIssuance) + return tecINTERNAL; // LCOV_EXCL_LINE + (*sleIssuance)[sfBallotID] = ballotKeylet.key; + view().update(sleIssuance); + } + + return tesSUCCESS; +} + +void +BallotCreate::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +BallotCreate::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/voting/BallotDelete.cpp b/src/libxrpl/tx/transactors/voting/BallotDelete.cpp new file mode 100644 index 00000000000..c87325f184c --- /dev/null +++ b/src/libxrpl/tx/transactors/voting/BallotDelete.cpp @@ -0,0 +1,119 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace xrpl { + +/** + * Seconds after CloseTime during which only finalization may delete a ballot. + */ +static constexpr std::uint32_t kBallotFinalizeGrace = 86400; + +NotTEC +BallotDelete::preflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfBallotID] == beast::kZero) + return temMALFORMED; + return tesSUCCESS; +} + +TER +BallotDelete::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + auto const ballotID = ctx.tx[sfBallotID]; + auto const now = ctx.view.parentCloseTime().time_since_epoch().count(); + + // A voter deletes their own BallotVote once the ballot has closed. + if (ctx.view.exists(keylet::ballotVote(ballotID, account))) + { + if (auto const sleBallot = ctx.view.read(keylet::ballot(ballotID)); + sleBallot && now < (*sleBallot)[sfCloseTime]) + return tecTOO_SOON; + return tesSUCCESS; + } + + // Otherwise the creator deletes the ballot itself. + auto const sleBallot = ctx.view.read(keylet::ballot(ballotID)); + if (!sleBallot) + return tecNO_ENTRY; + + if (sleBallot->getAccountID(sfOwner) != account) + return tecNO_PERMISSION; + + // Deletable once finalized. An unfinalized ballot is deletable only after a + // grace period past CloseTime, so the creator cannot delete a ballot the + // moment voting ends and deny everyone the published result. + if (!sleBallot->isFlag(lsfBallotFinalized) && + now < (*sleBallot)[sfCloseTime] + kBallotFinalizeGrace) + return tecTOO_SOON; + + return tesSUCCESS; +} + +TER +BallotDelete::doApply() +{ + auto const ballotID = ctx_.tx[sfBallotID]; + + auto const sleOwner = view().peek(keylet::account(accountID_)); + if (!sleOwner) + return tecINTERNAL; // LCOV_EXCL_LINE + + auto const deleteObject = [&](std::shared_ptr const& sle) -> TER { + std::uint64_t const page = (*sle)[sfOwnerNode]; + if (!view().dirRemove(keylet::ownerDir(accountID_), page, sle->key(), false)) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + decreaseOwnerCountForObject(view(), sleOwner, sle, 1, j_); + view().erase(sle); + return tesSUCCESS; + }; + + // Voter deleting their own cast record. + if (auto sleVote = view().peek(keylet::ballotVote(ballotID, accountID_))) + return deleteObject(sleVote); + + // Creator deleting the ballot. + auto sleBallot = view().peek(keylet::ballot(ballotID)); + if (!sleBallot) + return tecINTERNAL; // LCOV_EXCL_LINE + return deleteObject(sleBallot); +} + +void +BallotDelete::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +BallotDelete::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/libxrpl/tx/transactors/voting/BallotFinalize.cpp b/src/libxrpl/tx/transactors/voting/BallotFinalize.cpp new file mode 100644 index 00000000000..a768a5adc0c --- /dev/null +++ b/src/libxrpl/tx/transactors/voting/BallotFinalize.cpp @@ -0,0 +1,150 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace xrpl { + +NotTEC +BallotFinalize::preflight(PreflightContext const& ctx) +{ + auto const& results = ctx.tx.getFieldArray(sfResults); + if (results.empty() || results.size() > 8) + return temMALFORMED; + + for (auto const& entry : results) + { + if (!entry.isFieldPresent(sfBallotWeight)) + return temMALFORMED; + } + + // One decryption-correctness proof per option. + if (ctx.tx[sfZKProof].empty() || ctx.tx[sfZKProof].size() % kEcClawbackProofLength != 0) + return temMALFORMED; + + if (ctx.tx[sfZKProof].size() / kEcClawbackProofLength != results.size()) + return temMALFORMED; + + return tesSUCCESS; +} + +XRPAmount +BallotFinalize::calculateBaseFee(ReadView const& view, STTx const& tx) +{ + return Transactor::calculateBaseFee(view, tx, kConfidentialFeeMultiplier); +} + +TER +BallotFinalize::preclaim(PreclaimContext const& ctx) +{ + auto const account = ctx.tx[sfAccount]; + if (!ctx.view.exists(keylet::account(account))) + return terNO_ACCOUNT; + + auto const ballotID = ctx.tx[sfBallotID]; + auto const sleBallot = ctx.view.read(keylet::ballot(ballotID)); + if (!sleBallot) + return tecNO_ENTRY; + + if (sleBallot->isFlag(lsfBallotFinalized)) + return tecDUPLICATE; + + // Finalization is only valid once the voting window has closed. + if (ctx.view.parentCloseTime().time_since_epoch().count() < (*sleBallot)[sfCloseTime]) + return tecTOO_SOON; + + std::uint8_t const n = (*sleBallot)[sfOptionCount]; + auto const& results = ctx.tx.getFieldArray(sfResults); + if (results.size() != n) + return tecBALLOT_BAD_OPTIONS; + + Slice const proof = ctx.tx[sfZKProof]; + if (proof.size() != static_cast(n) * kEcClawbackProofLength) + return tecBALLOT_BAD_OPTIONS; + + STArray const& tally = sleBallot->getFieldArray(sfEncryptedTally); + if (tally.size() != n) + return tecINTERNAL; // LCOV_EXCL_LINE + + Slice const tallyKey = (*sleBallot)[sfTallyPublicKey]; + auto const ctxHash = + getBallotFinalizeContextHash(account, ballotID, ctx.tx.getSeqProxy().value()); + + // Each proof shows tally[i] decrypts to results[i] under the tally key. + for (std::uint8_t i = 0; i < n; ++i) + { + Slice const optionProof{ + proof.data() + (i * kEcClawbackProofLength), kEcClawbackProofLength}; + if (auto const ter = verifyClawbackProof( + results[i][sfBallotWeight], + optionProof, + tallyKey, + tally[i][sfEncryptedVote], + ctxHash); + !isTesSuccess(ter)) + { + return ter; + } + } + + return tesSUCCESS; +} + +TER +BallotFinalize::doApply() +{ + auto sleBallot = view().peek(keylet::ballot(ctx_.tx[sfBallotID])); + if (!sleBallot) + return tecINTERNAL; // LCOV_EXCL_LINE + + STArray results; + for (auto const& entry : ctx_.tx.getFieldArray(sfResults)) + { + STObject o = STObject::makeInnerObject(sfBallotResult); + o.setFieldU64(sfBallotWeight, entry[sfBallotWeight]); + results.push_back(std::move(o)); + } + sleBallot->setFieldArray(sfResults, results); + (*sleBallot)[sfFlags] = (*sleBallot)[sfFlags] | lsfBallotFinalized; + + view().update(sleBallot); + return tesSUCCESS; +} + +void +BallotFinalize::visitInvariantEntry( + bool, + std::shared_ptr const&, + std::shared_ptr const&) +{ +} + +bool +BallotFinalize::finalizeInvariants( + STTx const&, + TER, + XRPAmount, + ReadView const&, + beast::Journal const&) +{ + return true; +} + +} // namespace xrpl diff --git a/src/test/app/ConfidentialVoting_test.cpp b/src/test/app/ConfidentialVoting_test.cpp new file mode 100644 index 00000000000..90a50c0e0e5 --- /dev/null +++ b/src/test/app/ConfidentialVoting_test.cpp @@ -0,0 +1,652 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace xrpl { +namespace test { + +using namespace jtx; +using namespace std::chrono_literals; + +class ConfidentialVoting_test : public beast::unit_test::Suite +{ + // Build a BallotCreate transaction (token mode) as raw JSON. + static json::Value + createTokenBallot( + Account const& owner, + uint192 const& issuanceID, + ballot::Keypair const& tally, + std::uint8_t optionCount, + std::uint32_t openTime, + std::uint32_t closeTime, + uint256 const& ballotID) + { + json::Value jv; + jv[jss::TransactionType] = "BallotCreate"; + jv[jss::Account] = owner.human(); + jv[sfMPTokenIssuanceID.jsonName] = to_string(issuanceID); + jv[sfDigest.jsonName] = std::string(64, 'A'); + jv[sfOptionCount.jsonName] = optionCount; + jv[sfTallyPublicKey.jsonName] = strHex(tally.pub); + jv[sfOpenTime.jsonName] = openTime; + jv[sfCloseTime.jsonName] = closeTime; + jv[sfZKProof.jsonName] = strHex(ballot::schnorrProof(tally, ballotID)); + jv[jss::Fee] = "1000"; // covers the confidential fee multiplier + return jv; + } + + // Build a BallotCastVote transaction as raw JSON. + static json::Value + castVote(Account const& voter, uint256 const& ballotID, BallotCast const& cast) + { + json::Value jv; + jv[jss::TransactionType] = "BallotCastVote"; + jv[jss::Account] = voter.human(); + jv[sfBallotID.jsonName] = to_string(ballotID); + jv[sfEncryptedVotes.jsonName] = + ballotOptionArray(cast.ciphertexts, &cast.commitments, &cast.linkageProofs); + jv[sfZKProof.jsonName] = strHex(cast.proof); + jv[sfBlindingFactor.jsonName] = strHex(cast.blinding); + jv[jss::Fee] = "1000"; // covers the confidential fee multiplier + return jv; + } + + // Build a BallotFinalize transaction as raw JSON. + static json::Value + finalize( + Account const& authority, + uint256 const& ballotID, + std::vector const& results, + Buffer const& proof) + { + json::Value jv; + jv[jss::TransactionType] = "BallotFinalize"; + jv[jss::Account] = authority.human(); + jv[sfBallotID.jsonName] = to_string(ballotID); + json::Value arr(json::ValueType::Array); + for (auto const r : results) + { + json::Value inner; + // sfBallotWeight is a UINT64; its JSON string form is parsed as hex. + std::stringstream ss; + ss << std::hex << r; + inner[sfBallotWeight.jsonName] = ss.str(); + json::Value elem; + elem[sfBallotResult.jsonName] = inner; + arr.append(elem); + } + jv[sfResults.jsonName] = arr; + jv[sfZKProof.jsonName] = strHex(proof); + jv[jss::Fee] = "1000"; // covers the confidential fee multiplier + return jv; + } + + static json::Value + deleteBallot(Account const& account, uint256 const& ballotID) + { + json::Value jv; + jv[jss::TransactionType] = "BallotDelete"; + jv[jss::Account] = account.human(); + jv[sfBallotID.jsonName] = to_string(ballotID); + return jv; + } + + void + testAmendmentDisabled() + { + testcase("Amendment disabled"); + Env env{*this, testableAmendments() - featureConfidentialVoting}; + Account const alice{"alice"}; + env.fund(XRP(10000), alice); + env.close(); + + MPTTester mpt(env, alice, {.fund = false}); + mpt.create({.flags = tfMPTCanTransfer}); + + auto const tally = ballot::generateKeypair(); + auto const seq = env.seq(alice); + auto const ballotID = keylet::ballot(alice.id(), seq).key; + auto jv = createTokenBallot(alice, mpt.issuanceID(), tally, 2, 0, 0xFFFFFFFFu, ballotID); + env(jv, Ter(temDISABLED)); + } + + void + testTokenHappyPath() + { + testcase("Token mode happy path"); + Env env{*this}; + Account const alice{"alice"}; // issuer + tally authority + Account const bob{"bob"}; // voter + env.fund(XRP(10000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.holders = {bob}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const tally = ballot::generateKeypair(); + + // Create with a finite voting window. + auto const nowSecs = env.now().time_since_epoch().count(); + auto const createSeq = env.seq(alice); + auto const ballotID = keylet::ballot(alice.id(), createSeq).key; + env(createTokenBallot(alice, mpt.issuanceID(), tally, 2, 0, nowSecs + 1000, ballotID)); + env.close(); + + // Cast: bob assigns all 100 weight to option 0. + auto const castSeq = env.seq(bob); + auto const castCtx = getBallotCastContextHash(bob.id(), ballotID, castSeq); + auto const cast = makeBallotCast(tally, 2, /*choice=*/0, /*weight=*/100, castCtx); + env(castVote(bob, ballotID, cast)); + env.close(); + + // VoteCount incremented and lock recorded. + { + auto const sle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(sle && (*sle)[sfVoteCount] == 1); + auto const mptSle = env.le(keylet::mptoken(mpt.issuanceID(), bob.id())); + BEAST_EXPECT(mptSle && (*mptSle)[~sfVoteLockedAmount].value_or(0) == 100); + } + + // Close the voting window. + env.close(2000s); + + // Finalize: read the on-ledger tally and prove its decryption. + auto const sle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(sle); + auto const& tallyArr = sle->getFieldArray(sfEncryptedTally); + std::vector results; + std::vector proofBufs; + auto const finSeq = env.seq(alice); + auto const finCtx = getBallotFinalizeContextHash(alice.id(), ballotID, finSeq); + for (auto const& entry : tallyArr) + { + Slice const ct = entry[sfEncryptedVote]; + auto const amt = ballot::decrypt(ct, tally.priv, 1000); + BEAST_EXPECT(amt.has_value()); + results.push_back(*amt); + proofBufs.push_back(ballot::decryptionProof(tally, *amt, ct, finCtx)); + } + BEAST_EXPECT(results.size() == 2 && results[0] == 100 && results[1] == 0); + + Buffer proof(proofBufs.size() * kEcClawbackProofLength); + for (std::size_t i = 0; i < proofBufs.size(); ++i) + std::memcpy( + proof.data() + (i * kEcClawbackProofLength), + proofBufs[i].data(), + kEcClawbackProofLength); + + env(finalize(alice, ballotID, results, proof)); + env.close(); + + { + auto const finSle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(finSle && finSle->isFlag(lsfBallotFinalized)); + auto const& res = finSle->getFieldArray(sfResults); + BEAST_EXPECT( + res.size() == 2 && res[0][sfBallotWeight] == 100 && res[1][sfBallotWeight] == 0); + } + + // Lock released after close: bob can now transfer. + mpt.pay(bob, alice, 50); + + // Creator reclaims the ballot reserve. + env(deleteBallot(alice, ballotID)); + env.close(); + BEAST_EXPECT(!env.le(keylet::ballot(ballotID))); + } + + void + testCastFailures() + { + testcase("Cast failures"); + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + Account const carol{"carol"}; // no MPToken + env.fund(XRP(10000), alice, bob, carol); + env.close(); + + MPTTester mpt(env, alice, {.holders = {bob}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const tally = ballot::generateKeypair(); + + // A ballot that is not yet open. Uses a separate issuance so it does not + // collide with the open ballot below (one open ballot per issuance). + MPTTester mpt2(env, alice, {.holders = {bob}, .fund = false}); + mpt2.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + mpt2.authorize({.account = bob}); + mpt2.pay(alice, bob, 100); + env.close(); + + auto const nowSecs = env.now().time_since_epoch().count(); + auto const futureSeq = env.seq(alice); + auto const futureBallot = keylet::ballot(alice.id(), futureSeq).key; + env(createTokenBallot( + alice, mpt2.issuanceID(), tally, 2, nowSecs + 10000, nowSecs + 20000, futureBallot)); + env.close(); + { + auto const castSeq = env.seq(bob); + auto const cast = makeBallotCast( + tally, 2, 0, 100, getBallotCastContextHash(bob.id(), futureBallot, castSeq)); + env(castVote(bob, futureBallot, cast), Ter(tecBALLOT_NOT_OPEN)); + env.close(); + } + + // An open ballot for the remaining cases. + auto const openSeq = env.seq(alice); + auto const ballotID = keylet::ballot(alice.id(), openSeq).key; + env(createTokenBallot(alice, mpt.issuanceID(), tally, 2, 0, nowSecs + 5000, ballotID)); + env.close(); + + // Malformed proof: corrupt the range proof. + { + auto const castSeq = env.seq(bob); + auto cast = makeBallotCast( + tally, 2, 0, 100, getBallotCastContextHash(bob.id(), ballotID, castSeq)); + cast.proof.data()[0] ^= 0xFF; + env(castVote(bob, ballotID, cast), Ter(tecBAD_PROOF)); + env.close(); + } + + // Ineligible: carol holds no MPToken. + { + auto const castSeq = env.seq(carol); + auto const cast = makeBallotCast( + tally, 2, 0, 1, getBallotCastContextHash(carol.id(), ballotID, castSeq)); + env(castVote(carol, ballotID, cast), Ter(tecNO_ENTRY)); + env.close(); + } + + // Happy cast, then a double-cast is rejected. + { + auto const castSeq = env.seq(bob); + auto const cast = makeBallotCast( + tally, 2, 0, 100, getBallotCastContextHash(bob.id(), ballotID, castSeq)); + env(castVote(bob, ballotID, cast)); + env.close(); + + auto const castSeq2 = env.seq(bob); + auto const cast2 = makeBallotCast( + tally, 2, 0, 100, getBallotCastContextHash(bob.id(), ballotID, castSeq2)); + env(castVote(bob, ballotID, cast2), Ter(tecBALLOT_VOTED)); + env.close(); + } + + // Cast after close is rejected. + env.close(6000s); + { + Account const dave{"dave"}; + env.fund(XRP(10000), dave); + mpt.authorize({.account = dave}); + mpt.pay(alice, dave, 10); + auto const castSeq = env.seq(dave); + auto const cast = makeBallotCast( + tally, 2, 0, 10, getBallotCastContextHash(dave.id(), ballotID, castSeq)); + env(castVote(dave, ballotID, cast), Ter(tecBALLOT_CLOSED)); + env.close(); + } + } + + void + testFinalizeFailures() + { + testcase("Finalize failures"); + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.holders = {bob}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const tally = ballot::generateKeypair(); + auto const nowSecs = env.now().time_since_epoch().count(); + auto const createSeq = env.seq(alice); + auto const ballotID = keylet::ballot(alice.id(), createSeq).key; + env(createTokenBallot(alice, mpt.issuanceID(), tally, 2, 0, nowSecs + 3000, ballotID)); + env.close(); + + auto const castSeq = env.seq(bob); + auto const cast = + makeBallotCast(tally, 2, 0, 100, getBallotCastContextHash(bob.id(), ballotID, castSeq)); + env(castVote(bob, ballotID, cast)); + env.close(); + + // Finalize before close is rejected. + { + auto const sle = env.le(keylet::ballot(ballotID)); + auto const& tallyArr = sle->getFieldArray(sfEncryptedTally); + auto const finSeq = env.seq(alice); + auto const finCtx = getBallotFinalizeContextHash(alice.id(), ballotID, finSeq); + std::vector results{100, 0}; + Buffer proof(2 * kEcClawbackProofLength); + for (std::size_t i = 0; i < 2; ++i) + { + auto p = ballot::decryptionProof( + tally, results[i], tallyArr[i][sfEncryptedVote], finCtx); + std::memcpy( + proof.data() + (i * kEcClawbackProofLength), p.data(), kEcClawbackProofLength); + } + env(finalize(alice, ballotID, results, proof), Ter(tecTOO_SOON)); + env.close(); + } + + // Close the window. + env.close(4000s); + + // Finalize with wrong results is rejected: proofs are generated for the + // true counts (100, 0), but the submitted results claim (99, 0), so the + // decryption-correctness check fails. + { + auto const sle = env.le(keylet::ballot(ballotID)); + auto const& tallyArr = sle->getFieldArray(sfEncryptedTally); + auto const finSeq = env.seq(alice); + auto const finCtx = getBallotFinalizeContextHash(alice.id(), ballotID, finSeq); + std::vector const trueResults{100, 0}; + Buffer proof(2 * kEcClawbackProofLength); + for (std::size_t i = 0; i < 2; ++i) + { + auto p = ballot::decryptionProof( + tally, trueResults[i], tallyArr[i][sfEncryptedVote], finCtx); + std::memcpy( + proof.data() + (i * kEcClawbackProofLength), p.data(), kEcClawbackProofLength); + } + std::vector const wrong{99, 0}; + env(finalize(alice, ballotID, wrong, proof), Ter(tecBAD_PROOF)); + env.close(); + } + } + + // Low-level check that the compact_standard vote-linkage proof (with the + // canonical vacuous balance witness) verifies, and rejects a commitment to a + // different value than the ciphertext. + void + testLinkageRoundTrip() + { + testcase("Vote linkage compact_standard round-trip"); + auto* const ctx = mpt_secp256k1_context(); + + auto const tally = ballot::generateKeypair(); + std::uint64_t const m = 42; + auto const r = ballot::randomScalar(); + + // ct = Enc(m; r) under the tally key; split into C1, C2. + auto const ctBuf = ballot::encrypt(m, tally.pub, r); + auto const ct = makeEcPair(ctBuf); + BEAST_EXPECT(ct.has_value()); + + secp256k1_pubkey tallyPub; + BEAST_EXPECT( + secp256k1_ec_pubkey_parse(ctx, &tallyPub, tally.pub.data(), tally.pub.size()) == 1); + + // PC_m = m*G + r*H (blinded by the ElGamal randomness r). + auto const pcmBuf = ballot::pedersen(m, r); + secp256k1_pubkey pcm; + BEAST_EXPECT(secp256k1_ec_pubkey_parse(ctx, &pcm, pcmBuf.data(), pcmBuf.size()) == 1); + + // Vacuous balance witness: sk_A = 1, pk_A = G, b = 0, B1 = B2 = G, + // PC_b = rho_b*H. Constrains nothing about the vote. + Buffer skA(kEcScalarLength); // 0x00..01 + skA.data()[kEcScalarLength - 1] = 1; + secp256k1_pubkey pkA; + BEAST_EXPECT(secp256k1_ec_pubkey_create(ctx, &pkA, skA.data()) == 1); + secp256k1_pubkey const B1 = pkA; + secp256k1_pubkey const B2 = pkA; + auto const rhoB = ballot::randomScalar(); + auto const pcbBuf = ballot::pedersen(0, rhoB); + secp256k1_pubkey pcb; + BEAST_EXPECT(secp256k1_ec_pubkey_parse(ctx, &pcb, pcbBuf.data(), pcbBuf.size()) == 1); + + uint256 const ctxHash = sha512Half(std::uint16_t(0x5A5A), m); + + std::array proof{}; + BEAST_EXPECT( + secp256k1_compact_standard_prove( + ctx, + proof.data(), + m, + /*balance=*/0, + r.data(), + skA.data(), + rhoB.data(), + /*n=*/1, + &ct->c1, + &ct->c2, + &tallyPub, + &pcm, + &pkA, + &pcb, + &B1, + &B2, + ctxHash.data()) == 1); + + BEAST_EXPECT( + secp256k1_compact_standard_verify( + ctx, + proof.data(), + 1, + &ct->c1, + &ct->c2, + &tallyPub, + &pcm, + &pkA, + &pcb, + &B1, + &B2, + ctxHash.data()) == 1); + + // Tamper: PC_m commits to a different value than the ciphertext. + auto const pcmBadBuf = ballot::pedersen(m + 1, r); + secp256k1_pubkey pcmBad; + BEAST_EXPECT( + secp256k1_ec_pubkey_parse(ctx, &pcmBad, pcmBadBuf.data(), pcmBadBuf.size()) == 1); + BEAST_EXPECT( + secp256k1_compact_standard_verify( + ctx, + proof.data(), + 1, + &ct->c1, + &ct->c2, + &tallyPub, + &pcmBad, + &pkA, + &pcb, + &B1, + &B2, + ctxHash.data()) == 0); + } + + void + testCredentialHappyPath() + { + testcase("Credential mode (1p1v) happy path"); + Env env{*this}; + Account const issuer{"issuer"}; + Account const member{"member"}; + env.fund(XRP(10000), issuer, member); + env.close(); + + // Permissioned domain owned by issuer, accepting a MEMBER credential. + std::string const credType = "MEMBER"; + pdomain::Credentials const accepted{{.issuer = issuer, .credType = credType}}; + env(pdomain::setTx(issuer, accepted)); + auto const domainID = pdomain::getNewDomain(env.meta()); + env.close(); + + // Issue and accept the credential for the member. + env(credentials::create(member, issuer, credType)); + env.close(); + env(credentials::accept(member, issuer, credType)); + env.close(); + + auto const tally = ballot::generateKeypair(); + auto const nowSecs = env.now().time_since_epoch().count(); + auto const createSeq = env.seq(issuer); + auto const ballotID = keylet::ballot(issuer.id(), createSeq).key; + + json::Value jv; + jv[jss::TransactionType] = "BallotCreate"; + jv[jss::Account] = issuer.human(); + jv[sfDomainID.jsonName] = to_string(domainID); + jv[sfDigest.jsonName] = std::string(64, 'A'); + jv[sfOptionCount.jsonName] = 2; + jv[sfTallyPublicKey.jsonName] = strHex(tally.pub); + jv[sfOpenTime.jsonName] = 0; + jv[sfCloseTime.jsonName] = nowSecs + 1000; + jv[sfZKProof.jsonName] = strHex(ballot::schnorrProof(tally, ballotID)); + jv[jss::Fee] = "1000"; // covers the confidential fee multiplier + env(jv); + env.close(); + + // The member holds an accepted credential satisfying the domain, so + // weight is 1. The linkage proof makes the encrypted cast verifiable + // under the third-party tally key (no token, no lock). + auto const castSeq = env.seq(member); + auto const cast = makeBallotCast( + tally, + 2, + /*choice=*/0, + /*weight=*/1, + getBallotCastContextHash(member.id(), ballotID, castSeq)); + env(castVote(member, ballotID, cast)); + env.close(); + + { + auto const sle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(sle && (*sle)[sfVoteCount] == 1); + } + + // Close the window and finalize with per-option decryption proofs. + env.close(2000s); + auto const sle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(sle); + auto const& tallyArr = sle->getFieldArray(sfEncryptedTally); + std::vector results; + std::vector proofBufs; + auto const finSeq = env.seq(issuer); + auto const finCtx = getBallotFinalizeContextHash(issuer.id(), ballotID, finSeq); + for (auto const& entry : tallyArr) + { + Slice const ct = entry[sfEncryptedVote]; + auto const amt = ballot::decrypt(ct, tally.priv, 100); + BEAST_EXPECT(amt.has_value()); + results.push_back(*amt); + proofBufs.push_back(ballot::decryptionProof(tally, *amt, ct, finCtx)); + } + BEAST_EXPECT(results.size() == 2 && results[0] == 1 && results[1] == 0); + + Buffer proof(proofBufs.size() * kEcClawbackProofLength); + for (std::size_t i = 0; i < proofBufs.size(); ++i) + std::memcpy( + proof.data() + (i * kEcClawbackProofLength), + proofBufs[i].data(), + kEcClawbackProofLength); + + env(finalize(issuer, ballotID, results, proof)); + env.close(); + + auto const finSle = env.le(keylet::ballot(ballotID)); + BEAST_EXPECT(finSle && finSle->isFlag(lsfBallotFinalized)); + auto const& res = finSle->getFieldArray(sfResults); + BEAST_EXPECT(res.size() == 2 && res[0][sfBallotWeight] == 1 && res[1][sfBallotWeight] == 0); + } + +public: + void + testOptionCountAndGrace() + { + testcase("Option count and delete grace"); + using namespace jtx; + + Env env{*this}; + Account const alice{"alice"}; + Account const bob{"bob"}; + env.fund(XRP(10000), alice, bob); + env.close(); + + MPTTester mpt(env, alice, {.holders = {bob}, .fund = false}); + mpt.create({.flags = tfMPTCanTransfer | tfMPTCanLock}); + mpt.authorize({.account = bob}); + mpt.pay(alice, bob, 100); + env.close(); + + auto const tally = ballot::generateKeypair(); + auto const nowSecs = env.now().time_since_epoch().count(); + auto const seq = env.seq(alice); + auto const ballotID = keylet::ballot(alice.id(), seq).key; + env(createTokenBallot( + alice, mpt.issuanceID(), tally, 2, nowSecs - 10, nowSecs + 1000, ballotID)); + env.close(); + + // A vote vector that does not match the ballot's OptionCount is a state + // dependent failure, so it claims a fee rather than returning tem. + { + auto const castSeq = env.seq(bob); + auto const cast = makeBallotCast( + tally, 4, 0, 100, getBallotCastContextHash(bob.id(), ballotID, castSeq)); + env(castVote(bob, ballotID, cast), Ter(tecBALLOT_BAD_OPTIONS)); + env.close(); + } + + // The creator cannot delete an unfinalized ballot the moment voting + // ends; the result must stay claimable for the grace period. + env.close(std::chrono::seconds(1200)); + env(deleteBallot(alice, ballotID), Ter(tecTOO_SOON)); + env.close(); + + // Past the grace period it may be deleted. + env.close(std::chrono::seconds(90000)); + env(deleteBallot(alice, ballotID)); + env.close(); + BEAST_EXPECT(!env.le(keylet::ballot(ballotID))); + } + + void + run() override + { + testAmendmentDisabled(); + testTokenHappyPath(); + testCastFailures(); + testFinalizeFailures(); + testLinkageRoundTrip(); + testCredentialHappyPath(); + testOptionCountAndGrace(); + } +}; + +BEAST_DEFINE_TESTSUITE(ConfidentialVoting, app, xrpl); + +} // namespace test +} // namespace xrpl diff --git a/src/test/jtx/Ballot.h b/src/test/jtx/Ballot.h new file mode 100644 index 00000000000..4bdff7c2760 --- /dev/null +++ b/src/test/jtx/Ballot.h @@ -0,0 +1,339 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace xrpl { +namespace test { +namespace jtx { + +// Standalone confidential-voting crypto helpers for tests. Ballots use a tally +// key that no ledger account owns, so these build proofs directly against the +// merged mpt-crypto primitives rather than the per-account MPTTester helpers. +namespace ballot { + +struct Keypair +{ + Buffer priv; // 32 bytes + Buffer pub; // 33 bytes +}; + +inline Keypair +generateKeypair() +{ + Keypair kp{Buffer(kEcPrivKeyLength), Buffer(kEcPubKeyLength)}; + if (mpt_generate_keypair(kp.priv.data(), kp.pub.data()) != 0) + Throw("mpt_generate_keypair failed"); + return kp; +} + +inline Buffer +randomScalar() +{ + Buffer out(kEcBlindingFactorLength); + if (mpt_generate_blinding_factor(out.data()) != 0) + Throw("mpt_generate_blinding_factor failed"); + return out; +} + +inline Buffer +encrypt(std::uint64_t value, Slice const& pubKey, Slice const& randomness) +{ + auto out = encryptAmount(value, pubKey, randomness); + if (!out) + Throw("encryptAmount failed"); + return *out; +} + +inline Buffer +pedersen(std::uint64_t value, Slice const& blinding) +{ + Buffer out(kEcPedersenCommitmentLength); + if (mpt_get_pedersen_commitment(value, blinding.data(), out.data()) != 0) + Throw("mpt_get_pedersen_commitment failed"); + return out; +} + +// R = a + b (mod curve order). +inline Buffer +scalarAdd(Slice const& a, Slice const& b) +{ + Buffer out(kEcScalarLength); + secp256k1_mpt_scalar_add(out.data(), a.data(), b.data()); + return out; +} + +// Aggregated Bulletproof range proof over m committed option values. +inline Buffer +rangeProof( + std::vector const& values, + std::vector const& blindings, + uint256 const& contextHash) +{ + auto* const ctx = mpt_secp256k1_context(); + secp256k1_pubkey h; + secp256k1_mpt_get_h_generator(ctx, &h); + + std::vector flat(values.size() * kEcScalarLength); + for (std::size_t i = 0; i < blindings.size(); ++i) + std::memcpy(flat.data() + (i * kEcScalarLength), blindings[i].data(), kEcScalarLength); + + Buffer proof(kEcDoubleBulletproofLength * values.size()); + std::size_t proofLen = proof.size(); + if (secp256k1_bulletproof_prove_agg( + ctx, + proof.data(), + &proofLen, + values.data(), + flat.data(), + values.size(), + &h, + contextHash.data()) == 0) + { + Throw("secp256k1_bulletproof_prove_agg failed"); + } + + return Buffer(proof.data(), proofLen); +} + +// Schnorr proof of possession of the tally private key, bound to a context. +inline Buffer +schnorrProof(Keypair const& kp, uint256 const& contextHash) +{ + Buffer proof(kEcSchnorrProofLength); + if (mpt_get_convert_proof(kp.pub.data(), kp.priv.data(), contextHash.data(), proof.data()) != 0) + Throw("mpt_get_convert_proof failed"); + return proof; +} + +// Decryption-correctness (clawback) proof: proves ciphertext decrypts to amount +// under the tally key. +inline Buffer +decryptionProof( + Keypair const& kp, + std::uint64_t amount, + Slice const& ciphertext, + uint256 const& contextHash) +{ + Buffer proof(kEcClawbackProofLength); + if (mpt_get_clawback_proof( + kp.priv.data(), + kp.pub.data(), + contextHash.data(), + amount, + ciphertext.data(), + proof.data()) != 0) + { + Throw("mpt_get_clawback_proof failed"); + } + return proof; +} + +inline std::optional +decrypt(Slice const& ciphertext, Slice const& priv, std::uint64_t high) +{ + std::uint64_t out = 0; + if (mpt_decrypt_amount(ciphertext.data(), priv.data(), &out, 0, high) != 0) + return std::nullopt; + return out; +} + +// Per-option ciphertext<->commitment linkage proof. Proves each mirror +// ciphertext (tally first, then optional auditor/voter, all sharing the option +// randomness r) encrypts the value committed in PC_m = value*G + r*H. Uses the +// canonical vacuous balance witness (sk_A = 1, rho_b = 1) the verifier expects. +inline Buffer +linkageProof( + std::uint64_t value, + Slice const& r, + std::vector const& pubKeys, + std::vector const& ciphertexts, // one 66-byte ct per key, shared C1 + Slice const& commitment, + uint256 const& contextHash) +{ + auto* const ctx = mpt_secp256k1_context(); + std::size_t const n = pubKeys.size(); + + auto const first = makeEcPair(ciphertexts[0]); + if (!first) + Throw("linkageProof: bad tally ciphertext"); + secp256k1_pubkey const c1 = first->c1; + + std::vector c2v(n); + std::vector pkv(n); + for (std::size_t i = 0; i < n; ++i) + { + auto const pair = makeEcPair(ciphertexts[i]); + if (!pair) + Throw("linkageProof: bad mirror ciphertext"); + c2v[i] = pair->c2; + if (secp256k1_ec_pubkey_parse(ctx, &pkv[i], pubKeys[i].data(), pubKeys[i].size()) != 1) + Throw("linkageProof: bad pubkey"); + } + + secp256k1_pubkey pcm; + if (secp256k1_ec_pubkey_parse(ctx, &pcm, commitment.data(), commitment.size()) != 1) + Throw("linkageProof: bad commitment"); + + Buffer skA(kEcScalarLength); + skA.data()[kEcScalarLength - 1] = 1; + Buffer rhoB(kEcScalarLength); + rhoB.data()[kEcScalarLength - 1] = 1; + secp256k1_pubkey pkA; + if (secp256k1_ec_pubkey_create(ctx, &pkA, skA.data()) != 1) + Throw("linkageProof: pk_A"); + secp256k1_pubkey pcb; // PC_b = 0*G + 1*H = H + if (secp256k1_mpt_get_h_generator(ctx, &pcb) != 1) + Throw("linkageProof: H"); + + Buffer proof(kEcSendSigmaProofLength); + if (secp256k1_compact_standard_prove( + ctx, + proof.data(), + value, + /*balance=*/0, + r.data(), + skA.data(), + rhoB.data(), + n, + &c1, + c2v.data(), + pkv.data(), + &pcm, + &pkA, + &pcb, + &pkA, // B1 = G + &pkA, // B2 = G + contextHash.data()) != 1) + { + Throw("secp256k1_compact_standard_prove failed"); + } + return proof; +} + +} // namespace ballot + +// A cast's per-option payload, ready to be serialized into a BallotCastVote. +struct BallotCast +{ + std::vector ciphertexts; // one ElGamal ciphertext per option + std::vector commitments; // one Pedersen commitment per option + std::vector linkageProofs; // one compact-sigma proof per option + std::vector auditorCiphertexts; // empty unless auditor configured + std::vector voterCiphertexts; // empty unless voter-recoverable + Buffer proof; // aggregated range proof + Buffer blinding; // aggregate ElGamal randomness R +}; + +// Build a cast that assigns the full weight to option `choice` and zero to the +// rest, with real range proof and aggregate randomness. +inline BallotCast +makeBallotCast( + ballot::Keypair const& tally, + std::uint8_t optionCount, + std::uint8_t choice, + std::uint64_t weight, + uint256 const& contextHash, + std::optional const& auditor = std::nullopt, + std::optional const& voter = std::nullopt) +{ + BallotCast cast; + std::vector values(optionCount, 0); + values[choice] = weight; + + // A single per-option scalar r is the ElGamal randomness AND the Pedersen + // blinding, so the commitment PC = value*G + r*H links directly to the + // ciphertext for the compact-sigma linkage proof. + std::vector blindings; + Buffer aggregate(kEcScalarLength); // zero-initialized + + for (std::uint8_t i = 0; i < optionCount; ++i) + { + auto const r = ballot::randomScalar(); + blindings.push_back(r); + + cast.commitments.push_back(ballot::pedersen(values[i], r)); + cast.ciphertexts.push_back(ballot::encrypt(values[i], tally.pub, r)); + if (auditor) + cast.auditorCiphertexts.push_back(ballot::encrypt(values[i], auditor->pub, r)); + if (voter) + cast.voterCiphertexts.push_back(ballot::encrypt(values[i], voter->pub, r)); + + aggregate = ballot::scalarAdd(aggregate, r); + } + + cast.blinding = aggregate; + cast.proof = ballot::rangeProof(values, blindings, contextHash); + + // Per-option linkage proof over every mirror key present (tally first). + for (std::uint8_t i = 0; i < optionCount; ++i) + { + std::vector keys{tally.pub}; + std::vector cts{cast.ciphertexts[i]}; + if (auditor) + { + keys.push_back(auditor->pub); + cts.push_back(cast.auditorCiphertexts[i]); + } + if (voter) + { + keys.push_back(voter->pub); + cts.push_back(cast.voterCiphertexts[i]); + } + cast.linkageProofs.push_back( + ballot::linkageProof( + values[i], blindings[i], keys, cts, cast.commitments[i], contextHash)); + } + return cast; +} + +// Serialize a ciphertext vector as a JSON array of BallotOption inner objects. +inline json::Value +ballotOptionArray( + std::vector const& ciphertexts, + std::vector const* commitments, + std::vector const* proofs = nullptr) +{ + json::Value arr(json::ValueType::Array); + for (std::size_t i = 0; i < ciphertexts.size(); ++i) + { + json::Value inner; + inner[sfEncryptedVote.jsonName] = strHex(ciphertexts[i]); + if (commitments) + inner[sfAmountCommitment.jsonName] = strHex((*commitments)[i]); + if (proofs) + inner[sfZKProof.jsonName] = strHex((*proofs)[i]); + json::Value elem; + elem[sfBallotOption.jsonName] = inner; + arr.append(elem); + } + return arr; +} + +} // namespace jtx +} // namespace test +} // namespace xrpl diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotTests.cpp new file mode 100644 index 00000000000..ef0aa65ff0d --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotTests.cpp @@ -0,0 +1,520 @@ +// Auto-generated unit tests for ledger entry Ballot + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(BallotTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const ownerValue = canonical_ACCOUNT(); + auto const sequenceValue = canonical_UINT32(); + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const domainIDValue = canonical_UINT256(); + auto const digestValue = canonical_UINT256(); + auto const uRIValue = canonical_VL(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const encryptedTallyValue = canonical_ARRAY(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const voteCountValue = canonical_UINT32(); + auto const resultsValue = canonical_ARRAY(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + BallotBuilder builder{ + ownerValue, + sequenceValue, + digestValue, + optionCountValue, + tallyPublicKeyValue, + encryptedTallyValue, + openTimeValue, + closeTimeValue, + ownerNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + builder.setMPTokenIssuanceID(mPTokenIssuanceIDValue); + builder.setDomainID(domainIDValue); + builder.setURI(uRIValue); + builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); + builder.setVoteCount(voteCountValue); + builder.setResults(resultsValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = ownerValue; + auto const actual = entry.getOwner(); + expectEqualField(expected, actual, "sfOwner"); + } + + { + auto const& expected = sequenceValue; + auto const actual = entry.getSequence(); + expectEqualField(expected, actual, "sfSequence"); + } + + { + auto const& expected = digestValue; + auto const actual = entry.getDigest(); + expectEqualField(expected, actual, "sfDigest"); + } + + { + auto const& expected = optionCountValue; + auto const actual = entry.getOptionCount(); + expectEqualField(expected, actual, "sfOptionCount"); + } + + { + auto const& expected = tallyPublicKeyValue; + auto const actual = entry.getTallyPublicKey(); + expectEqualField(expected, actual, "sfTallyPublicKey"); + } + + { + auto const& expected = encryptedTallyValue; + auto const actual = entry.getEncryptedTally(); + expectEqualField(expected, actual, "sfEncryptedTally"); + } + + { + auto const& expected = openTimeValue; + auto const actual = entry.getOpenTime(); + expectEqualField(expected, actual, "sfOpenTime"); + } + + { + auto const& expected = closeTimeValue; + auto const actual = entry.getCloseTime(); + expectEqualField(expected, actual, "sfCloseTime"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actualOpt = entry.getMPTokenIssuanceID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfMPTokenIssuanceID"); + EXPECT_TRUE(entry.hasMPTokenIssuanceID()); + } + + { + auto const& expected = domainIDValue; + auto const actualOpt = entry.getDomainID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfDomainID"); + EXPECT_TRUE(entry.hasDomainID()); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = entry.getURI(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfURI"); + EXPECT_TRUE(entry.hasURI()); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = entry.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + EXPECT_TRUE(entry.hasAuditorEncryptionKey()); + } + + { + auto const& expected = voteCountValue; + auto const actualOpt = entry.getVoteCount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVoteCount"); + EXPECT_TRUE(entry.hasVoteCount()); + } + + { + auto const& expected = resultsValue; + auto const actualOpt = entry.getResults(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfResults"); + EXPECT_TRUE(entry.hasResults()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(BallotTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const ownerValue = canonical_ACCOUNT(); + auto const sequenceValue = canonical_UINT32(); + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const domainIDValue = canonical_UINT256(); + auto const digestValue = canonical_UINT256(); + auto const uRIValue = canonical_VL(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const encryptedTallyValue = canonical_ARRAY(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const voteCountValue = canonical_UINT32(); + auto const resultsValue = canonical_ARRAY(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + auto sle = std::make_shared(Ballot::entryType, index); + + sle->at(sfOwner) = ownerValue; + sle->at(sfSequence) = sequenceValue; + sle->at(sfMPTokenIssuanceID) = mPTokenIssuanceIDValue; + sle->at(sfDomainID) = domainIDValue; + sle->at(sfDigest) = digestValue; + sle->at(sfURI) = uRIValue; + sle->at(sfOptionCount) = optionCountValue; + sle->at(sfTallyPublicKey) = tallyPublicKeyValue; + sle->at(sfAuditorEncryptionKey) = auditorEncryptionKeyValue; + sle->setFieldArray(sfEncryptedTally, encryptedTallyValue); + sle->at(sfOpenTime) = openTimeValue; + sle->at(sfCloseTime) = closeTimeValue; + sle->at(sfVoteCount) = voteCountValue; + sle->setFieldArray(sfResults, resultsValue); + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + + BallotBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + Ballot entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = ownerValue; + + auto const fromSle = entryFromSle.getOwner(); + auto const fromBuilder = entryFromBuilder.getOwner(); + + expectEqualField(expected, fromSle, "sfOwner"); + expectEqualField(expected, fromBuilder, "sfOwner"); + } + + { + auto const& expected = sequenceValue; + + auto const fromSle = entryFromSle.getSequence(); + auto const fromBuilder = entryFromBuilder.getSequence(); + + expectEqualField(expected, fromSle, "sfSequence"); + expectEqualField(expected, fromBuilder, "sfSequence"); + } + + { + auto const& expected = digestValue; + + auto const fromSle = entryFromSle.getDigest(); + auto const fromBuilder = entryFromBuilder.getDigest(); + + expectEqualField(expected, fromSle, "sfDigest"); + expectEqualField(expected, fromBuilder, "sfDigest"); + } + + { + auto const& expected = optionCountValue; + + auto const fromSle = entryFromSle.getOptionCount(); + auto const fromBuilder = entryFromBuilder.getOptionCount(); + + expectEqualField(expected, fromSle, "sfOptionCount"); + expectEqualField(expected, fromBuilder, "sfOptionCount"); + } + + { + auto const& expected = tallyPublicKeyValue; + + auto const fromSle = entryFromSle.getTallyPublicKey(); + auto const fromBuilder = entryFromBuilder.getTallyPublicKey(); + + expectEqualField(expected, fromSle, "sfTallyPublicKey"); + expectEqualField(expected, fromBuilder, "sfTallyPublicKey"); + } + + { + auto const& expected = encryptedTallyValue; + + auto const fromSle = entryFromSle.getEncryptedTally(); + auto const fromBuilder = entryFromBuilder.getEncryptedTally(); + + expectEqualField(expected, fromSle, "sfEncryptedTally"); + expectEqualField(expected, fromBuilder, "sfEncryptedTally"); + } + + { + auto const& expected = openTimeValue; + + auto const fromSle = entryFromSle.getOpenTime(); + auto const fromBuilder = entryFromBuilder.getOpenTime(); + + expectEqualField(expected, fromSle, "sfOpenTime"); + expectEqualField(expected, fromBuilder, "sfOpenTime"); + } + + { + auto const& expected = closeTimeValue; + + auto const fromSle = entryFromSle.getCloseTime(); + auto const fromBuilder = entryFromBuilder.getCloseTime(); + + expectEqualField(expected, fromSle, "sfCloseTime"); + expectEqualField(expected, fromBuilder, "sfCloseTime"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = mPTokenIssuanceIDValue; + + auto const fromSleOpt = entryFromSle.getMPTokenIssuanceID(); + auto const fromBuilderOpt = entryFromBuilder.getMPTokenIssuanceID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfMPTokenIssuanceID"); + expectEqualField(expected, *fromBuilderOpt, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = domainIDValue; + + auto const fromSleOpt = entryFromSle.getDomainID(); + auto const fromBuilderOpt = entryFromBuilder.getDomainID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfDomainID"); + expectEqualField(expected, *fromBuilderOpt, "sfDomainID"); + } + + { + auto const& expected = uRIValue; + + auto const fromSleOpt = entryFromSle.getURI(); + auto const fromBuilderOpt = entryFromBuilder.getURI(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfURI"); + expectEqualField(expected, *fromBuilderOpt, "sfURI"); + } + + { + auto const& expected = auditorEncryptionKeyValue; + + auto const fromSleOpt = entryFromSle.getAuditorEncryptionKey(); + auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptionKey(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptionKey"); + expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptionKey"); + } + + { + auto const& expected = voteCountValue; + + auto const fromSleOpt = entryFromSle.getVoteCount(); + auto const fromBuilderOpt = entryFromBuilder.getVoteCount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVoteCount"); + expectEqualField(expected, *fromBuilderOpt, "sfVoteCount"); + } + + { + auto const& expected = resultsValue; + + auto const fromSleOpt = entryFromSle.getResults(); + auto const fromBuilderOpt = entryFromBuilder.getResults(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfResults"); + expectEqualField(expected, *fromBuilderOpt, "sfResults"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(BallotTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(Ballot{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(BallotTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(BallotBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(BallotTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const ownerValue = canonical_ACCOUNT(); + auto const sequenceValue = canonical_UINT32(); + auto const digestValue = canonical_UINT256(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const encryptedTallyValue = canonical_ARRAY(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + BallotBuilder builder{ + ownerValue, + sequenceValue, + digestValue, + optionCountValue, + tallyPublicKeyValue, + encryptedTallyValue, + openTimeValue, + closeTimeValue, + ownerNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasMPTokenIssuanceID()); + EXPECT_FALSE(entry.getMPTokenIssuanceID().has_value()); + EXPECT_FALSE(entry.hasDomainID()); + EXPECT_FALSE(entry.getDomainID().has_value()); + EXPECT_FALSE(entry.hasURI()); + EXPECT_FALSE(entry.getURI().has_value()); + EXPECT_FALSE(entry.hasAuditorEncryptionKey()); + EXPECT_FALSE(entry.getAuditorEncryptionKey().has_value()); + EXPECT_FALSE(entry.hasVoteCount()); + EXPECT_FALSE(entry.getVoteCount().has_value()); + EXPECT_FALSE(entry.hasResults()); + EXPECT_FALSE(entry.getResults().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotVoteTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotVoteTests.cpp new file mode 100644 index 00000000000..92e71298950 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/BallotVoteTests.cpp @@ -0,0 +1,351 @@ +// Auto-generated unit tests for ledger entry BallotVote + + +#include + +#include + +#include +#include +#include + +#include + +namespace xrpl::ledger_entries { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed for both the +// builder's STObject and the wrapper's SLE. +TEST(BallotVoteTests, BuilderSettersRoundTrip) +{ + uint256 const index{1u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const ballotIDValue = canonical_UINT256(); + auto const ballotWeightValue = canonical_UINT64(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const auditorEncryptedVotesValue = canonical_ARRAY(); + auto const voterPublicKeyValue = canonical_VL(); + auto const voterEncryptedVotesValue = canonical_ARRAY(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + BallotVoteBuilder builder{ + accountValue, + ballotIDValue, + ballotWeightValue, + encryptedVotesValue, + ownerNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + builder.setAuditorEncryptedVotes(auditorEncryptedVotesValue); + builder.setVoterPublicKey(voterPublicKeyValue); + builder.setVoterEncryptedVotes(voterEncryptedVotesValue); + + builder.setLedgerIndex(index); + builder.setFlags(0x1u); + + EXPECT_TRUE(builder.validate()); + + auto const entry = builder.build(index); + + EXPECT_TRUE(entry.validate()); + + { + auto const& expected = accountValue; + auto const actual = entry.getAccount(); + expectEqualField(expected, actual, "sfAccount"); + } + + { + auto const& expected = ballotIDValue; + auto const actual = entry.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + { + auto const& expected = ballotWeightValue; + auto const actual = entry.getBallotWeight(); + expectEqualField(expected, actual, "sfBallotWeight"); + } + + { + auto const& expected = encryptedVotesValue; + auto const actual = entry.getEncryptedVotes(); + expectEqualField(expected, actual, "sfEncryptedVotes"); + } + + { + auto const& expected = ownerNodeValue; + auto const actual = entry.getOwnerNode(); + expectEqualField(expected, actual, "sfOwnerNode"); + } + + { + auto const& expected = previousTxnIDValue; + auto const actual = entry.getPreviousTxnID(); + expectEqualField(expected, actual, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + auto const actual = entry.getPreviousTxnLgrSeq(); + expectEqualField(expected, actual, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = auditorEncryptedVotesValue; + auto const actualOpt = entry.getAuditorEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedVotes"); + EXPECT_TRUE(entry.hasAuditorEncryptedVotes()); + } + + { + auto const& expected = voterPublicKeyValue; + auto const actualOpt = entry.getVoterPublicKey(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVoterPublicKey"); + EXPECT_TRUE(entry.hasVoterPublicKey()); + } + + { + auto const& expected = voterEncryptedVotesValue; + auto const actualOpt = entry.getVoterEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVoterEncryptedVotes"); + EXPECT_TRUE(entry.hasVoterEncryptedVotes()); + } + + EXPECT_TRUE(entry.hasLedgerIndex()); + auto const ledgerIndex = entry.getLedgerIndex(); + ASSERT_TRUE(ledgerIndex.has_value()); + EXPECT_EQ(*ledgerIndex, index); + EXPECT_EQ(entry.getKey(), index); +} + +// 2 & 4) Start from an SLE, set fields directly on it, construct a builder +// from that SLE, build a new wrapper, and verify all fields (and validate()). +TEST(BallotVoteTests, BuilderFromSleRoundTrip) +{ + uint256 const index{2u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const ballotIDValue = canonical_UINT256(); + auto const ballotWeightValue = canonical_UINT64(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const auditorEncryptedVotesValue = canonical_ARRAY(); + auto const voterPublicKeyValue = canonical_VL(); + auto const voterEncryptedVotesValue = canonical_ARRAY(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + auto sle = std::make_shared(BallotVote::entryType, index); + + sle->at(sfAccount) = accountValue; + sle->at(sfBallotID) = ballotIDValue; + sle->at(sfBallotWeight) = ballotWeightValue; + sle->setFieldArray(sfEncryptedVotes, encryptedVotesValue); + sle->setFieldArray(sfAuditorEncryptedVotes, auditorEncryptedVotesValue); + sle->at(sfVoterPublicKey) = voterPublicKeyValue; + sle->setFieldArray(sfVoterEncryptedVotes, voterEncryptedVotesValue); + sle->at(sfOwnerNode) = ownerNodeValue; + sle->at(sfPreviousTxnID) = previousTxnIDValue; + sle->at(sfPreviousTxnLgrSeq) = previousTxnLgrSeqValue; + + BallotVoteBuilder builderFromSle{sle}; + EXPECT_TRUE(builderFromSle.validate()); + + auto const entryFromBuilder = builderFromSle.build(index); + + BallotVote entryFromSle{sle}; + EXPECT_TRUE(entryFromBuilder.validate()); + EXPECT_TRUE(entryFromSle.validate()); + + { + auto const& expected = accountValue; + + auto const fromSle = entryFromSle.getAccount(); + auto const fromBuilder = entryFromBuilder.getAccount(); + + expectEqualField(expected, fromSle, "sfAccount"); + expectEqualField(expected, fromBuilder, "sfAccount"); + } + + { + auto const& expected = ballotIDValue; + + auto const fromSle = entryFromSle.getBallotID(); + auto const fromBuilder = entryFromBuilder.getBallotID(); + + expectEqualField(expected, fromSle, "sfBallotID"); + expectEqualField(expected, fromBuilder, "sfBallotID"); + } + + { + auto const& expected = ballotWeightValue; + + auto const fromSle = entryFromSle.getBallotWeight(); + auto const fromBuilder = entryFromBuilder.getBallotWeight(); + + expectEqualField(expected, fromSle, "sfBallotWeight"); + expectEqualField(expected, fromBuilder, "sfBallotWeight"); + } + + { + auto const& expected = encryptedVotesValue; + + auto const fromSle = entryFromSle.getEncryptedVotes(); + auto const fromBuilder = entryFromBuilder.getEncryptedVotes(); + + expectEqualField(expected, fromSle, "sfEncryptedVotes"); + expectEqualField(expected, fromBuilder, "sfEncryptedVotes"); + } + + { + auto const& expected = ownerNodeValue; + + auto const fromSle = entryFromSle.getOwnerNode(); + auto const fromBuilder = entryFromBuilder.getOwnerNode(); + + expectEqualField(expected, fromSle, "sfOwnerNode"); + expectEqualField(expected, fromBuilder, "sfOwnerNode"); + } + + { + auto const& expected = previousTxnIDValue; + + auto const fromSle = entryFromSle.getPreviousTxnID(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnID(); + + expectEqualField(expected, fromSle, "sfPreviousTxnID"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnID"); + } + + { + auto const& expected = previousTxnLgrSeqValue; + + auto const fromSle = entryFromSle.getPreviousTxnLgrSeq(); + auto const fromBuilder = entryFromBuilder.getPreviousTxnLgrSeq(); + + expectEqualField(expected, fromSle, "sfPreviousTxnLgrSeq"); + expectEqualField(expected, fromBuilder, "sfPreviousTxnLgrSeq"); + } + + { + auto const& expected = auditorEncryptedVotesValue; + + auto const fromSleOpt = entryFromSle.getAuditorEncryptedVotes(); + auto const fromBuilderOpt = entryFromBuilder.getAuditorEncryptedVotes(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfAuditorEncryptedVotes"); + expectEqualField(expected, *fromBuilderOpt, "sfAuditorEncryptedVotes"); + } + + { + auto const& expected = voterPublicKeyValue; + + auto const fromSleOpt = entryFromSle.getVoterPublicKey(); + auto const fromBuilderOpt = entryFromBuilder.getVoterPublicKey(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVoterPublicKey"); + expectEqualField(expected, *fromBuilderOpt, "sfVoterPublicKey"); + } + + { + auto const& expected = voterEncryptedVotesValue; + + auto const fromSleOpt = entryFromSle.getVoterEncryptedVotes(); + auto const fromBuilderOpt = entryFromBuilder.getVoterEncryptedVotes(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVoterEncryptedVotes"); + expectEqualField(expected, *fromBuilderOpt, "sfVoterEncryptedVotes"); + } + + EXPECT_EQ(entryFromSle.getKey(), index); + EXPECT_EQ(entryFromBuilder.getKey(), index); +} + +// 3) Verify wrapper throws when constructed from wrong ledger entry type. +TEST(BallotVoteTests, WrapperThrowsOnWrongEntryType) +{ + uint256 const index{3u}; + + // Build a valid ledger entry of a different type + // Ticket requires: Account, OwnerNode, TicketSequence, PreviousTxnID, PreviousTxnLgrSeq + // Check requires: Account, Destination, SendMax, Sequence, OwnerNode, DestinationNode, PreviousTxnID, PreviousTxnLgrSeq + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(BallotVote{wrongEntry.getSle()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong ledger entry type. +TEST(BallotVoteTests, BuilderThrowsOnWrongEntryType) +{ + uint256 const index{4u}; + + // Build a valid ledger entry of a different type + TicketBuilder wrongBuilder{ + canonical_ACCOUNT(), + canonical_UINT64(), + canonical_UINT32(), + canonical_UINT256(), + canonical_UINT32()}; + auto wrongEntry = wrongBuilder.build(index); + + EXPECT_THROW(BallotVoteBuilder{wrongEntry.getSle()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(BallotVoteTests, OptionalFieldsReturnNullopt) +{ + uint256 const index{3u}; + + auto const accountValue = canonical_ACCOUNT(); + auto const ballotIDValue = canonical_UINT256(); + auto const ballotWeightValue = canonical_UINT64(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const ownerNodeValue = canonical_UINT64(); + auto const previousTxnIDValue = canonical_UINT256(); + auto const previousTxnLgrSeqValue = canonical_UINT32(); + + BallotVoteBuilder builder{ + accountValue, + ballotIDValue, + ballotWeightValue, + encryptedVotesValue, + ownerNodeValue, + previousTxnIDValue, + previousTxnLgrSeqValue + }; + + auto const entry = builder.build(index); + + // Verify optional fields are not present + EXPECT_FALSE(entry.hasAuditorEncryptedVotes()); + EXPECT_FALSE(entry.getAuditorEncryptedVotes().has_value()); + EXPECT_FALSE(entry.hasVoterPublicKey()); + EXPECT_FALSE(entry.getVoterPublicKey().has_value()); + EXPECT_FALSE(entry.hasVoterEncryptedVotes()); + EXPECT_FALSE(entry.getVoterEncryptedVotes().has_value()); +} +} diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp index e8c1b645d84..1c551bd905d 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenIssuanceTests.cpp @@ -39,6 +39,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) auto const issuerKeyEpochValue = canonical_UINT32(); auto const auditorKeyEpochValue = canonical_UINT32(); auto const confidentialOutstandingAmountValue = canonical_UINT64(); + auto const ballotIDValue = canonical_UINT256(); MPTokenIssuanceBuilder builder{ issuerValue, @@ -62,6 +63,7 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) builder.setIssuerKeyEpoch(issuerKeyEpochValue); builder.setAuditorKeyEpoch(auditorKeyEpochValue); builder.setConfidentialOutstandingAmount(confidentialOutstandingAmountValue); + builder.setBallotID(ballotIDValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -212,6 +214,14 @@ TEST(MPTokenIssuanceTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasConfidentialOutstandingAmount()); } + { + auto const& expected = ballotIDValue; + auto const actualOpt = entry.getBallotID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfBallotID"); + EXPECT_TRUE(entry.hasBallotID()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -244,6 +254,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) auto const issuerKeyEpochValue = canonical_UINT32(); auto const auditorKeyEpochValue = canonical_UINT32(); auto const confidentialOutstandingAmountValue = canonical_UINT64(); + auto const ballotIDValue = canonical_UINT256(); auto sle = std::make_shared(MPTokenIssuance::entryType, index); @@ -266,6 +277,7 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) sle->at(sfIssuerKeyEpoch) = issuerKeyEpochValue; sle->at(sfAuditorKeyEpoch) = auditorKeyEpochValue; sle->at(sfConfidentialOutstandingAmount) = confidentialOutstandingAmountValue; + sle->at(sfBallotID) = ballotIDValue; MPTokenIssuanceBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -505,6 +517,19 @@ TEST(MPTokenIssuanceTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfConfidentialOutstandingAmount"); } + { + auto const& expected = ballotIDValue; + + auto const fromSleOpt = entryFromSle.getBallotID(); + auto const fromBuilderOpt = entryFromBuilder.getBallotID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfBallotID"); + expectEqualField(expected, *fromBuilderOpt, "sfBallotID"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -595,5 +620,7 @@ TEST(MPTokenIssuanceTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getAuditorKeyEpoch().has_value()); EXPECT_FALSE(entry.hasConfidentialOutstandingAmount()); EXPECT_FALSE(entry.getConfidentialOutstandingAmount().has_value()); + EXPECT_FALSE(entry.hasBallotID()); + EXPECT_FALSE(entry.getBallotID().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp index 7db4b638a7f..cc758a8d909 100644 --- a/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp +++ b/src/tests/libxrpl/protocol_autogen/ledger_entries/MPTokenTests.cpp @@ -33,6 +33,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); auto const holderEncryptionKeyValue = canonical_VL(); + auto const voteLockedAmountValue = canonical_UINT64(); + auto const ballotIDValue = canonical_UINT256(); MPTokenBuilder builder{ accountValue, @@ -50,6 +52,8 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) builder.setIssuerEncryptedBalance(issuerEncryptedBalanceValue); builder.setAuditorEncryptedBalance(auditorEncryptedBalanceValue); builder.setHolderEncryptionKey(holderEncryptionKeyValue); + builder.setVoteLockedAmount(voteLockedAmountValue); + builder.setBallotID(ballotIDValue); builder.setLedgerIndex(index); builder.setFlags(0x1u); @@ -154,6 +158,22 @@ TEST(MPTokenTests, BuilderSettersRoundTrip) EXPECT_TRUE(entry.hasHolderEncryptionKey()); } + { + auto const& expected = voteLockedAmountValue; + auto const actualOpt = entry.getVoteLockedAmount(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfVoteLockedAmount"); + EXPECT_TRUE(entry.hasVoteLockedAmount()); + } + + { + auto const& expected = ballotIDValue; + auto const actualOpt = entry.getBallotID(); + ASSERT_TRUE(actualOpt.has_value()); + expectEqualField(expected, *actualOpt, "sfBallotID"); + EXPECT_TRUE(entry.hasBallotID()); + } + EXPECT_TRUE(entry.hasLedgerIndex()); auto const ledgerIndex = entry.getLedgerIndex(); ASSERT_TRUE(ledgerIndex.has_value()); @@ -180,6 +200,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) auto const issuerEncryptedBalanceValue = canonical_VL(); auto const auditorEncryptedBalanceValue = canonical_VL(); auto const holderEncryptionKeyValue = canonical_VL(); + auto const voteLockedAmountValue = canonical_UINT64(); + auto const ballotIDValue = canonical_UINT256(); auto sle = std::make_shared(MPToken::entryType, index); @@ -196,6 +218,8 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) sle->at(sfIssuerEncryptedBalance) = issuerEncryptedBalanceValue; sle->at(sfAuditorEncryptedBalance) = auditorEncryptedBalanceValue; sle->at(sfHolderEncryptionKey) = holderEncryptionKeyValue; + sle->at(sfVoteLockedAmount) = voteLockedAmountValue; + sle->at(sfBallotID) = ballotIDValue; MPTokenBuilder builderFromSle{sle}; EXPECT_TRUE(builderFromSle.validate()); @@ -360,6 +384,32 @@ TEST(MPTokenTests, BuilderFromSleRoundTrip) expectEqualField(expected, *fromBuilderOpt, "sfHolderEncryptionKey"); } + { + auto const& expected = voteLockedAmountValue; + + auto const fromSleOpt = entryFromSle.getVoteLockedAmount(); + auto const fromBuilderOpt = entryFromBuilder.getVoteLockedAmount(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfVoteLockedAmount"); + expectEqualField(expected, *fromBuilderOpt, "sfVoteLockedAmount"); + } + + { + auto const& expected = ballotIDValue; + + auto const fromSleOpt = entryFromSle.getBallotID(); + auto const fromBuilderOpt = entryFromBuilder.getBallotID(); + + ASSERT_TRUE(fromSleOpt.has_value()); + ASSERT_TRUE(fromBuilderOpt.has_value()); + + expectEqualField(expected, *fromSleOpt, "sfBallotID"); + expectEqualField(expected, *fromBuilderOpt, "sfBallotID"); + } + EXPECT_EQ(entryFromSle.getKey(), index); EXPECT_EQ(entryFromBuilder.getKey(), index); } @@ -438,5 +488,9 @@ TEST(MPTokenTests, OptionalFieldsReturnNullopt) EXPECT_FALSE(entry.getAuditorEncryptedBalance().has_value()); EXPECT_FALSE(entry.hasHolderEncryptionKey()); EXPECT_FALSE(entry.getHolderEncryptionKey().has_value()); + EXPECT_FALSE(entry.hasVoteLockedAmount()); + EXPECT_FALSE(entry.getVoteLockedAmount().has_value()); + EXPECT_FALSE(entry.hasBallotID()); + EXPECT_FALSE(entry.getBallotID().has_value()); } } diff --git a/src/tests/libxrpl/protocol_autogen/transactions/BallotCastVoteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/BallotCastVoteTests.cpp new file mode 100644 index 00000000000..a192de47870 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/BallotCastVoteTests.cpp @@ -0,0 +1,312 @@ +// Auto-generated unit tests for transaction BallotCastVote + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsBallotCastVoteTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCastVote")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const auditorEncryptedVotesValue = canonical_ARRAY(); + auto const voterPublicKeyValue = canonical_VL(); + auto const voterEncryptedVotesValue = canonical_ARRAY(); + auto const zKProofValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const credentialIDsValue = canonical_VECTOR256(); + + BallotCastVoteBuilder builder{ + accountValue, + ballotIDValue, + encryptedVotesValue, + zKProofValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setAuditorEncryptedVotes(auditorEncryptedVotesValue); + builder.setVoterPublicKey(voterPublicKeyValue); + builder.setVoterEncryptedVotes(voterEncryptedVotesValue); + builder.setCredentialIDs(credentialIDsValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = tx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + { + auto const& expected = encryptedVotesValue; + auto const actual = tx.getEncryptedVotes(); + expectEqualField(expected, actual, "sfEncryptedVotes"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = tx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + // Verify optional fields + { + auto const& expected = auditorEncryptedVotesValue; + auto const actualOpt = tx.getAuditorEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedVotes should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedVotes"); + EXPECT_TRUE(tx.hasAuditorEncryptedVotes()); + } + + { + auto const& expected = voterPublicKeyValue; + auto const actualOpt = tx.getVoterPublicKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVoterPublicKey should be present"; + expectEqualField(expected, *actualOpt, "sfVoterPublicKey"); + EXPECT_TRUE(tx.hasVoterPublicKey()); + } + + { + auto const& expected = voterEncryptedVotesValue; + auto const actualOpt = tx.getVoterEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVoterEncryptedVotes should be present"; + expectEqualField(expected, *actualOpt, "sfVoterEncryptedVotes"); + EXPECT_TRUE(tx.hasVoterEncryptedVotes()); + } + + { + auto const& expected = credentialIDsValue; + auto const actualOpt = tx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + EXPECT_TRUE(tx.hasCredentialIDs()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsBallotCastVoteTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCastVoteFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const auditorEncryptedVotesValue = canonical_ARRAY(); + auto const voterPublicKeyValue = canonical_VL(); + auto const voterEncryptedVotesValue = canonical_ARRAY(); + auto const zKProofValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + auto const credentialIDsValue = canonical_VECTOR256(); + + // Build an initial transaction + BallotCastVoteBuilder initialBuilder{ + accountValue, + ballotIDValue, + encryptedVotesValue, + zKProofValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + initialBuilder.setAuditorEncryptedVotes(auditorEncryptedVotesValue); + initialBuilder.setVoterPublicKey(voterPublicKeyValue); + initialBuilder.setVoterEncryptedVotes(voterEncryptedVotesValue); + initialBuilder.setCredentialIDs(credentialIDsValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + BallotCastVoteBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = rebuiltTx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + { + auto const& expected = encryptedVotesValue; + auto const actual = rebuiltTx.getEncryptedVotes(); + expectEqualField(expected, actual, "sfEncryptedVotes"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + { + auto const& expected = blindingFactorValue; + auto const actual = rebuiltTx.getBlindingFactor(); + expectEqualField(expected, actual, "sfBlindingFactor"); + } + + // Verify optional fields + { + auto const& expected = auditorEncryptedVotesValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptedVotes should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptedVotes"); + } + + { + auto const& expected = voterPublicKeyValue; + auto const actualOpt = rebuiltTx.getVoterPublicKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVoterPublicKey should be present"; + expectEqualField(expected, *actualOpt, "sfVoterPublicKey"); + } + + { + auto const& expected = voterEncryptedVotesValue; + auto const actualOpt = rebuiltTx.getVoterEncryptedVotes(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfVoterEncryptedVotes should be present"; + expectEqualField(expected, *actualOpt, "sfVoterEncryptedVotes"); + } + + { + auto const& expected = credentialIDsValue; + auto const actualOpt = rebuiltTx.getCredentialIDs(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfCredentialIDs should be present"; + expectEqualField(expected, *actualOpt, "sfCredentialIDs"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsBallotCastVoteTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotCastVote{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsBallotCastVoteTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotCastVoteBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsBallotCastVoteTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCastVoteNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const ballotIDValue = canonical_UINT256(); + auto const encryptedVotesValue = canonical_ARRAY(); + auto const zKProofValue = canonical_VL(); + auto const blindingFactorValue = canonical_UINT256(); + + BallotCastVoteBuilder builder{ + accountValue, + ballotIDValue, + encryptedVotesValue, + zKProofValue, + blindingFactorValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasAuditorEncryptedVotes()); + EXPECT_FALSE(tx.getAuditorEncryptedVotes().has_value()); + EXPECT_FALSE(tx.hasVoterPublicKey()); + EXPECT_FALSE(tx.getVoterPublicKey().has_value()); + EXPECT_FALSE(tx.hasVoterEncryptedVotes()); + EXPECT_FALSE(tx.getVoterEncryptedVotes().has_value()); + EXPECT_FALSE(tx.hasCredentialIDs()); + EXPECT_FALSE(tx.getCredentialIDs().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/BallotCreateTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/BallotCreateTests.cpp new file mode 100644 index 00000000000..10f2e8dc67f --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/BallotCreateTests.cpp @@ -0,0 +1,348 @@ +// Auto-generated unit tests for transaction BallotCreate + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsBallotCreateTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCreate")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const domainIDValue = canonical_UINT256(); + auto const digestValue = canonical_UINT256(); + auto const uRIValue = canonical_VL(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const zKProofValue = canonical_VL(); + + BallotCreateBuilder builder{ + accountValue, + digestValue, + optionCountValue, + tallyPublicKeyValue, + openTimeValue, + closeTimeValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Set optional fields + builder.setMPTokenIssuanceID(mPTokenIssuanceIDValue); + builder.setDomainID(domainIDValue); + builder.setURI(uRIValue); + builder.setAuditorEncryptionKey(auditorEncryptionKeyValue); + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = digestValue; + auto const actual = tx.getDigest(); + expectEqualField(expected, actual, "sfDigest"); + } + + { + auto const& expected = optionCountValue; + auto const actual = tx.getOptionCount(); + expectEqualField(expected, actual, "sfOptionCount"); + } + + { + auto const& expected = tallyPublicKeyValue; + auto const actual = tx.getTallyPublicKey(); + expectEqualField(expected, actual, "sfTallyPublicKey"); + } + + { + auto const& expected = openTimeValue; + auto const actual = tx.getOpenTime(); + expectEqualField(expected, actual, "sfOpenTime"); + } + + { + auto const& expected = closeTimeValue; + auto const actual = tx.getCloseTime(); + expectEqualField(expected, actual, "sfCloseTime"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actualOpt = tx.getMPTokenIssuanceID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMPTokenIssuanceID should be present"; + expectEqualField(expected, *actualOpt, "sfMPTokenIssuanceID"); + EXPECT_TRUE(tx.hasMPTokenIssuanceID()); + } + + { + auto const& expected = domainIDValue; + auto const actualOpt = tx.getDomainID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDomainID should be present"; + expectEqualField(expected, *actualOpt, "sfDomainID"); + EXPECT_TRUE(tx.hasDomainID()); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = tx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + EXPECT_TRUE(tx.hasURI()); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = tx.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + EXPECT_TRUE(tx.hasAuditorEncryptionKey()); + } + +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsBallotCreateTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCreateFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const mPTokenIssuanceIDValue = canonical_UINT192(); + auto const domainIDValue = canonical_UINT256(); + auto const digestValue = canonical_UINT256(); + auto const uRIValue = canonical_VL(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const auditorEncryptionKeyValue = canonical_VL(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const zKProofValue = canonical_VL(); + + // Build an initial transaction + BallotCreateBuilder initialBuilder{ + accountValue, + digestValue, + optionCountValue, + tallyPublicKeyValue, + openTimeValue, + closeTimeValue, + zKProofValue, + sequenceValue, + feeValue + }; + + initialBuilder.setMPTokenIssuanceID(mPTokenIssuanceIDValue); + initialBuilder.setDomainID(domainIDValue); + initialBuilder.setURI(uRIValue); + initialBuilder.setAuditorEncryptionKey(auditorEncryptionKeyValue); + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + BallotCreateBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = digestValue; + auto const actual = rebuiltTx.getDigest(); + expectEqualField(expected, actual, "sfDigest"); + } + + { + auto const& expected = optionCountValue; + auto const actual = rebuiltTx.getOptionCount(); + expectEqualField(expected, actual, "sfOptionCount"); + } + + { + auto const& expected = tallyPublicKeyValue; + auto const actual = rebuiltTx.getTallyPublicKey(); + expectEqualField(expected, actual, "sfTallyPublicKey"); + } + + { + auto const& expected = openTimeValue; + auto const actual = rebuiltTx.getOpenTime(); + expectEqualField(expected, actual, "sfOpenTime"); + } + + { + auto const& expected = closeTimeValue; + auto const actual = rebuiltTx.getCloseTime(); + expectEqualField(expected, actual, "sfCloseTime"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields + { + auto const& expected = mPTokenIssuanceIDValue; + auto const actualOpt = rebuiltTx.getMPTokenIssuanceID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfMPTokenIssuanceID should be present"; + expectEqualField(expected, *actualOpt, "sfMPTokenIssuanceID"); + } + + { + auto const& expected = domainIDValue; + auto const actualOpt = rebuiltTx.getDomainID(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfDomainID should be present"; + expectEqualField(expected, *actualOpt, "sfDomainID"); + } + + { + auto const& expected = uRIValue; + auto const actualOpt = rebuiltTx.getURI(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfURI should be present"; + expectEqualField(expected, *actualOpt, "sfURI"); + } + + { + auto const& expected = auditorEncryptionKeyValue; + auto const actualOpt = rebuiltTx.getAuditorEncryptionKey(); + ASSERT_TRUE(actualOpt.has_value()) << "Optional field sfAuditorEncryptionKey should be present"; + expectEqualField(expected, *actualOpt, "sfAuditorEncryptionKey"); + } + +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsBallotCreateTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotCreate{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsBallotCreateTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotCreateBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + +// 5) Build with only required fields and verify optional fields return nullopt. +TEST(TransactionsBallotCreateTests, OptionalFieldsReturnNullopt) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotCreateNullopt")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 3; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific required field values + auto const digestValue = canonical_UINT256(); + auto const optionCountValue = canonical_UINT8(); + auto const tallyPublicKeyValue = canonical_VL(); + auto const openTimeValue = canonical_UINT32(); + auto const closeTimeValue = canonical_UINT32(); + auto const zKProofValue = canonical_VL(); + + BallotCreateBuilder builder{ + accountValue, + digestValue, + optionCountValue, + tallyPublicKeyValue, + openTimeValue, + closeTimeValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Do NOT set optional fields + + auto tx = builder.build(publicKey, secretKey); + + // Verify optional fields are not present + EXPECT_FALSE(tx.hasMPTokenIssuanceID()); + EXPECT_FALSE(tx.getMPTokenIssuanceID().has_value()); + EXPECT_FALSE(tx.hasDomainID()); + EXPECT_FALSE(tx.getDomainID().has_value()); + EXPECT_FALSE(tx.hasURI()); + EXPECT_FALSE(tx.getURI().has_value()); + EXPECT_FALSE(tx.hasAuditorEncryptionKey()); + EXPECT_FALSE(tx.getAuditorEncryptionKey().has_value()); +} + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/BallotDeleteTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/BallotDeleteTests.cpp new file mode 100644 index 00000000000..2393cd48f10 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/BallotDeleteTests.cpp @@ -0,0 +1,146 @@ +// Auto-generated unit tests for transaction BallotDelete + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsBallotDeleteTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotDelete")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + + BallotDeleteBuilder builder{ + accountValue, + ballotIDValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = tx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsBallotDeleteTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotDeleteFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + + // Build an initial transaction + BallotDeleteBuilder initialBuilder{ + accountValue, + ballotIDValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + BallotDeleteBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = rebuiltTx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsBallotDeleteTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotDelete{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsBallotDeleteTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotDeleteBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/tests/libxrpl/protocol_autogen/transactions/BallotFinalizeTests.cpp b/src/tests/libxrpl/protocol_autogen/transactions/BallotFinalizeTests.cpp new file mode 100644 index 00000000000..71301075e70 --- /dev/null +++ b/src/tests/libxrpl/protocol_autogen/transactions/BallotFinalizeTests.cpp @@ -0,0 +1,178 @@ +// Auto-generated unit tests for transaction BallotFinalize + + +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace xrpl::transactions { + +// 1 & 4) Set fields via builder setters, build, then read them back via +// wrapper getters. After build(), validate() should succeed. +TEST(TransactionsBallotFinalizeTests, BuilderSettersRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotFinalize")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 1; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + auto const resultsValue = canonical_ARRAY(); + auto const zKProofValue = canonical_VL(); + + BallotFinalizeBuilder builder{ + accountValue, + ballotIDValue, + resultsValue, + zKProofValue, + sequenceValue, + feeValue + }; + + // Set optional fields + + auto tx = builder.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(tx.validate(reason)) << reason; + + // Verify signing was applied + EXPECT_FALSE(tx.getSigningPubKey().empty()); + EXPECT_TRUE(tx.hasTxnSignature()); + + // Verify common fields + EXPECT_EQ(tx.getAccount(), accountValue); + EXPECT_EQ(tx.getSequence(), sequenceValue); + EXPECT_EQ(tx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = tx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + { + auto const& expected = resultsValue; + auto const actual = tx.getResults(); + expectEqualField(expected, actual, "sfResults"); + } + + { + auto const& expected = zKProofValue; + auto const actual = tx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields +} + +// 2 & 4) Start from an STTx, construct a builder from it, build a new wrapper, +// and verify all fields match. +TEST(TransactionsBallotFinalizeTests, BuilderFromStTxRoundTrip) +{ + // Generate a deterministic keypair for signing + auto const [publicKey, secretKey] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testBallotFinalizeFromTx")); + + // Common transaction fields + auto const accountValue = calcAccountID(publicKey); + std::uint32_t const sequenceValue = 2; + auto const feeValue = canonical_AMOUNT(); + + // Transaction-specific field values + auto const ballotIDValue = canonical_UINT256(); + auto const resultsValue = canonical_ARRAY(); + auto const zKProofValue = canonical_VL(); + + // Build an initial transaction + BallotFinalizeBuilder initialBuilder{ + accountValue, + ballotIDValue, + resultsValue, + zKProofValue, + sequenceValue, + feeValue + }; + + + auto initialTx = initialBuilder.build(publicKey, secretKey); + + // Create builder from existing STTx + BallotFinalizeBuilder builderFromTx{initialTx.getSTTx()}; + + auto rebuiltTx = builderFromTx.build(publicKey, secretKey); + + std::string reason; + EXPECT_TRUE(rebuiltTx.validate(reason)) << reason; + + // Verify common fields + EXPECT_EQ(rebuiltTx.getAccount(), accountValue); + EXPECT_EQ(rebuiltTx.getSequence(), sequenceValue); + EXPECT_EQ(rebuiltTx.getFee(), feeValue); + + // Verify required fields + { + auto const& expected = ballotIDValue; + auto const actual = rebuiltTx.getBallotID(); + expectEqualField(expected, actual, "sfBallotID"); + } + + { + auto const& expected = resultsValue; + auto const actual = rebuiltTx.getResults(); + expectEqualField(expected, actual, "sfResults"); + } + + { + auto const& expected = zKProofValue; + auto const actual = rebuiltTx.getZKProof(); + expectEqualField(expected, actual, "sfZKProof"); + } + + // Verify optional fields +} + +// 3) Verify wrapper throws when constructed from wrong transaction type. +TEST(TransactionsBallotFinalizeTests, WrapperThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongType")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotFinalize{wrongTx.getSTTx()}, std::runtime_error); +} + +// 4) Verify builder throws when constructed from wrong transaction type. +TEST(TransactionsBallotFinalizeTests, BuilderThrowsOnWrongTxType) +{ + // Build a valid transaction of a different type + auto const [pk, sk] = + generateKeyPair(KeyType::Secp256k1, generateSeed("testWrongTypeBuilder")); + auto const account = calcAccountID(pk); + + AccountSetBuilder wrongBuilder{account, 1, canonical_AMOUNT()}; + auto wrongTx = wrongBuilder.build(pk, sk); + + EXPECT_THROW(BallotFinalizeBuilder{wrongTx.getSTTx()}, std::runtime_error); +} + + +} diff --git a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp index 5271720b34f..2310103c95f 100644 --- a/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp +++ b/src/xrpld/rpc/handlers/ledger/LedgerEntry.cpp @@ -524,6 +524,51 @@ parseLoan( return keylet::loan(*id, seqProxy).key; } +static std::expected +parseBallot( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName, "hex string"); + } + + auto const owner = + ledger_entry_helpers::requiredAccountID(params, jss::owner, "malformedOwner"); + if (!owner) + return std::unexpected(owner.error()); + auto const seq = ledger_entry_helpers::requiredUInt32(params, jss::seq, "malformedSeq"); + if (!seq) + return std::unexpected(seq.error()); + + return keylet::ballot(*owner, *seq).key; +} + +static std::expected +parseBallotVote( + json::Value const& params, + json::StaticString const fieldName, + [[maybe_unused]] unsigned const apiVersion) +{ + if (!params.isObject()) + { + return parseObjectID(params, fieldName, "hex string"); + } + + auto const ballotID = + ledger_entry_helpers::requiredUInt256(params, jss::ballot_id, "malformedBallotID"); + if (!ballotID) + return std::unexpected(ballotID.error()); + auto const account = + ledger_entry_helpers::requiredAccountID(params, jss::account, "malformedAccount"); + if (!account) + return std::unexpected(account.error()); + + return keylet::ballotVote(*ballotID, *account).key; +} + static std::expected parseMPToken( json::Value const& params,