diff --git a/confidential/builder/clawback_test.go b/confidential/builder/clawback_test.go index af812132f..42ae735df 100644 --- a/confidential/builder/clawback_test.go +++ b/confidential/builder/clawback_test.go @@ -1,3 +1,5 @@ +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) + package builder import ( diff --git a/confidential/builder/convert_back.go b/confidential/builder/convert_back.go index 9637b23a4..c3f0edaa7 100644 --- a/confidential/builder/convert_back.go +++ b/confidential/builder/convert_back.go @@ -13,13 +13,15 @@ import ( // BuildConvertBackParams holds minimal inputs for BuildConvertBack. // Sequence, IssuerPubKey, AuditorPubKey, BalanceVersion, CurrentBalanceCt, -// and CurrentBalance are auto-resolved from the ledger. Balance is decrypted using HolderPrivKey. +// and CurrentBalance are auto-resolved from the ledger. Balance is decrypted using HolderPrivKey +// within BalanceRange's inclusive bounds. type BuildConvertBackParams struct { Account string IssuanceID string Amount uint64 - HolderPrivKey string // 64 hex chars, also used to decrypt balance from ledger - HolderPubKey string // 66 hex chars (compressed) + HolderPrivKey string // 64 hex chars, also used to decrypt balance from ledger + HolderPubKey string // 66 hex chars (compressed) + BalanceRange elgamal.AmountRange // Inclusive balance decryption bounds } // ConvertBackParams holds inputs for PrepareConvertBack. @@ -70,6 +72,9 @@ func BuildConvertBack(q LedgerQuerier, p BuildConvertBackParams) (*transaction.C if err := validateConvertBackBase(p); err != nil { return nil, err } + if err := p.BalanceRange.Validate(); err != nil { + return nil, err + } seq, err := getSequence(q, p.Account) if err != nil { @@ -90,7 +95,7 @@ func BuildConvertBack(q LedgerQuerier, p BuildConvertBackParams) (*transaction.C return nil, fmt.Errorf("%w: holder pubkey does not match ledger", ErrCryptoFailed) } - currentBalance, err := elgamal.Decrypt(balanceCt, p.HolderPrivKey) + currentBalance, err := elgamal.Decrypt(balanceCt, p.HolderPrivKey, p.BalanceRange) if err != nil { return nil, fmt.Errorf("%w: failed to decrypt balance: %w", ErrCryptoFailed, err) } diff --git a/confidential/builder/convert_back_test.go b/confidential/builder/convert_back_test.go index 87af3a23c..f532d6466 100644 --- a/confidential/builder/convert_back_test.go +++ b/confidential/builder/convert_back_test.go @@ -1,3 +1,5 @@ +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) + package builder import ( @@ -6,8 +8,6 @@ import ( "github.com/Peersyst/xrpl-go/confidential/elgamal" "github.com/Peersyst/xrpl-go/confidential/proof" - xrplhash "github.com/Peersyst/xrpl-go/xrpl/hash" - ledgerentries "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types" "github.com/Peersyst/xrpl-go/xrpl/transaction" "github.com/stretchr/testify/require" ) @@ -172,41 +172,55 @@ func TestPrepareConvertBack_FailValidation(t *testing.T) { } } -func TestBuildConvertBack_Pass(t *testing.T) { - holderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - +func TestBuildConvertBack_BalanceRange(t *testing.T) { const currentBalance uint64 = 1000 - const withdrawAmount uint64 = 100 - bf, err := elgamal.GenerateBlindingFactor() - require.NoError(t, err) - balanceCt, err := elgamal.Encrypt(currentBalance, holderKP.PubKeyHex, bf) - require.NoError(t, err) - - issuanceIndex, err := xrplhash.MPTokenIssuance(testIssuanceID) - require.NoError(t, err) - mptokenIndex, err := xrplhash.MPToken(testIssuanceID, testAccount) - require.NoError(t, err) + tests := []struct { + name string + balanceRange elgamal.AmountRange + wantErr bool + }{ + {name: "pass - balance in range", balanceRange: elgamal.AmountRange{Low: currentBalance, High: currentBalance}}, + {name: "fail - balance outside range", balanceRange: elgamal.AmountRange{Low: 0, High: currentBalance - 1}, wantErr: true}, + } - q := &mockQuerier{ - accountSeq: 3, - entries: map[string]ledgerentries.FlatLedgerObject{ - issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), - mptokenIndex: buildMPTokenEntry(holderKP.PubKeyHex, balanceCt, 1, ""), - }, + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + holderKP, q := newBalanceLedgerFixture(t, 3, 1, currentBalance) + result, err := BuildConvertBack(q, BuildConvertBackParams{ + Account: testAccount, + IssuanceID: testIssuanceID, + Amount: 100, + HolderPrivKey: holderKP.PrivKeyHex, + HolderPubKey: holderKP.PubKeyHex, + BalanceRange: tt.balanceRange, + }) + if tt.wantErr { + require.ErrorIs(t, err, ErrCryptoFailed) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) + return + } + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, uint32(3), result.Sequence) + }) } +} + +func TestBuildConvertBack_InvalidRangeBeforeLedgerQueries(t *testing.T) { + holderKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) - result, err := BuildConvertBack(q, BuildConvertBackParams{ + q := &mockQuerier{accountErr: ErrLedgerQuery} + _, err = BuildConvertBack(q, BuildConvertBackParams{ Account: testAccount, IssuanceID: testIssuanceID, - Amount: withdrawAmount, + Amount: 1, HolderPrivKey: holderKP.PrivKeyHex, HolderPubKey: holderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: 2, High: 1}, }) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, uint32(3), result.Sequence) + require.ErrorIs(t, err, elgamal.ErrInvalidAmountRange) + require.NotErrorIs(t, err, ErrLedgerQuery) + require.Zero(t, q.queryCalls, "invalid ranges must fail before ledger queries") } diff --git a/confidential/builder/convert_test.go b/confidential/builder/convert_test.go index bbbcad6b1..88c893339 100644 --- a/confidential/builder/convert_test.go +++ b/confidential/builder/convert_test.go @@ -1,3 +1,5 @@ +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) + package builder import ( diff --git a/confidential/builder/helpers_test.go b/confidential/builder/helpers_test.go index 2a61024b6..87be8c086 100644 --- a/confidential/builder/helpers_test.go +++ b/confidential/builder/helpers_test.go @@ -1,9 +1,14 @@ package builder import ( + "testing" + + "github.com/Peersyst/xrpl-go/confidential/elgamal" + xrplhash "github.com/Peersyst/xrpl-go/xrpl/hash" ledgerentries "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types" "github.com/Peersyst/xrpl-go/xrpl/queries/account" "github.com/Peersyst/xrpl-go/xrpl/queries/ledger" + "github.com/stretchr/testify/require" ) const ( @@ -17,9 +22,12 @@ type mockQuerier struct { accountSeq uint32 accountErr error // when set, GetAccountInfo returns this error entries map[string]ledgerentries.FlatLedgerObject + // queryCalls counts ledger requests so tests can assert failures occur before unnecessary ledger access. + queryCalls int } func (m *mockQuerier) GetAccountInfo(_ *account.InfoRequest) (*account.InfoResponse, error) { + m.queryCalls++ if m.accountErr != nil { return nil, m.accountErr } @@ -29,6 +37,7 @@ func (m *mockQuerier) GetAccountInfo(_ *account.InfoRequest) (*account.InfoRespo } func (m *mockQuerier) GetLedgerEntry(req *ledger.EntryRequest) (*ledger.EntryResponse, error) { + m.queryCalls++ node, ok := m.entries[req.Index] if !ok { return nil, ErrMPTokenNotFound @@ -64,3 +73,29 @@ func buildMPTokenEntry(holderKey, balanceCt string, balanceVersion float64, issu } return entry } + +func newBalanceLedgerFixture(t *testing.T, sequence uint32, balanceVersion float64, balance uint64) (elgamal.Keypair, *mockQuerier) { + t.Helper() + + ownerKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + issuerKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + blindingFactor, err := elgamal.GenerateBlindingFactor() + require.NoError(t, err) + balanceCt, err := elgamal.Encrypt(balance, ownerKP.PubKeyHex, blindingFactor) + require.NoError(t, err) + + issuanceIndex, err := xrplhash.MPTokenIssuance(testIssuanceID) + require.NoError(t, err) + mptokenIndex, err := xrplhash.MPToken(testIssuanceID, testAccount) + require.NoError(t, err) + + return ownerKP, &mockQuerier{ + accountSeq: sequence, + entries: map[string]ledgerentries.FlatLedgerObject{ + issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), + mptokenIndex: buildMPTokenEntry(ownerKP.PubKeyHex, balanceCt, balanceVersion, ""), + }, + } +} diff --git a/confidential/builder/send.go b/confidential/builder/send.go index 31d6ff733..a72b20ec2 100644 --- a/confidential/builder/send.go +++ b/confidential/builder/send.go @@ -13,15 +13,17 @@ import ( // BuildSendParams holds minimal inputs for BuildSend. // Sequence, ReceiverPubKey, IssuerPubKey, AuditorPubKey, BalanceVersion, CurrentBalanceCt, -// and CurrentBalance are auto-resolved from the ledger. Balance is decrypted using SenderPrivKey. +// and CurrentBalance are auto-resolved from the ledger. Balance is decrypted using SenderPrivKey +// within BalanceRange's inclusive bounds. type BuildSendParams struct { Account string Destination string IssuanceID string Amount uint64 - SenderPrivKey string // 64 hex chars, also used to decrypt balance from ledger - SenderPubKey string // 66 hex chars (compressed) - CredentialIDs []string // Optional + SenderPrivKey string // 64 hex chars, also used to decrypt balance from ledger + SenderPubKey string // 66 hex chars (compressed) + BalanceRange elgamal.AmountRange // Inclusive balance decryption bounds + CredentialIDs []string // Optional } // SendParams holds inputs for PrepareSend. @@ -82,6 +84,9 @@ func BuildSend(q LedgerQuerier, p BuildSendParams) (*transaction.ConfidentialMPT if err := validateSendBase(p); err != nil { return nil, err } + if err := p.BalanceRange.Validate(); err != nil { + return nil, err + } seq, err := getSequence(q, p.Account) if err != nil { @@ -103,7 +108,7 @@ func BuildSend(q LedgerQuerier, p BuildSendParams) (*transaction.ConfidentialMPT return nil, fmt.Errorf("%w: sender pubkey does not match ledger", ErrCryptoFailed) } - currentBalance, err := elgamal.Decrypt(senderBalanceCt, p.SenderPrivKey) + currentBalance, err := elgamal.Decrypt(senderBalanceCt, p.SenderPrivKey, p.BalanceRange) if err != nil { return nil, fmt.Errorf("%w: failed to decrypt balance: %w", ErrCryptoFailed, err) } diff --git a/confidential/builder/send_test.go b/confidential/builder/send_test.go index 202ddb038..72d11891d 100644 --- a/confidential/builder/send_test.go +++ b/confidential/builder/send_test.go @@ -1,3 +1,5 @@ +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) + package builder import ( @@ -270,36 +272,15 @@ func TestPrepareSend_FailValidation(t *testing.T) { } func TestBuildSend_Pass(t *testing.T) { - senderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - receiverKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - const currentBalance uint64 = 1000 const sendAmount uint64 = 300 - bf, err := elgamal.GenerateBlindingFactor() - require.NoError(t, err) - senderBalanceCt, err := elgamal.Encrypt(currentBalance, senderKP.PubKeyHex, bf) - require.NoError(t, err) - - issuanceIndex, err := xrplhash.MPTokenIssuance(testIssuanceID) - require.NoError(t, err) - senderMPTIndex, err := xrplhash.MPToken(testIssuanceID, testAccount) + senderKP, q := newBalanceLedgerFixture(t, 8, 2, currentBalance) + receiverKP, err := elgamal.GenerateKeypair() require.NoError(t, err) receiverMPTIndex, err := xrplhash.MPToken(testIssuanceID, testDestination) require.NoError(t, err) - - q := &mockQuerier{ - accountSeq: 8, - entries: map[string]ledgerentries.FlatLedgerObject{ - issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), - senderMPTIndex: buildMPTokenEntry(senderKP.PubKeyHex, senderBalanceCt, 2, ""), - receiverMPTIndex: buildMPTokenEntry(receiverKP.PubKeyHex, "", 0, ""), - }, - } + q.entries[receiverMPTIndex] = buildMPTokenEntry(receiverKP.PubKeyHex, "", 0, "") result, err := BuildSend(q, BuildSendParams{ Account: testAccount, @@ -308,6 +289,7 @@ func TestBuildSend_Pass(t *testing.T) { Amount: sendAmount, SenderPrivKey: senderKP.PrivKeyHex, SenderPubKey: senderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: currentBalance, High: currentBalance}, }) require.NoError(t, err) require.NotNil(t, result) @@ -315,6 +297,42 @@ func TestBuildSend_Pass(t *testing.T) { require.NotEmpty(t, result.ZKProof) } +func TestBuildSend_FailBalanceOutsideRange(t *testing.T) { + const currentBalance uint64 = 1000 + + senderKP, q := newBalanceLedgerFixture(t, 8, 2, currentBalance) + _, err := BuildSend(q, BuildSendParams{ + Account: testAccount, + Destination: testDestination, + IssuanceID: testIssuanceID, + Amount: 300, + SenderPrivKey: senderKP.PrivKeyHex, + SenderPubKey: senderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: 0, High: currentBalance - 1}, + }) + require.ErrorIs(t, err, ErrCryptoFailed) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) +} + +func TestBuildSend_InvalidRangeBeforeLedgerQueries(t *testing.T) { + senderKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + + q := &mockQuerier{accountErr: ErrLedgerQuery} + _, err = BuildSend(q, BuildSendParams{ + Account: testAccount, + Destination: testDestination, + IssuanceID: testIssuanceID, + Amount: 1, + SenderPrivKey: senderKP.PrivKeyHex, + SenderPubKey: senderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: 2, High: 1}, + }) + require.ErrorIs(t, err, elgamal.ErrInvalidAmountRange) + require.NotErrorIs(t, err, ErrLedgerQuery) + require.Zero(t, q.queryCalls, "invalid ranges must fail before ledger queries") +} + func TestBuildSend_FailReceiverNotOptedIn(t *testing.T) { senderKP, err := elgamal.GenerateKeypair() require.NoError(t, err) @@ -348,6 +366,7 @@ func TestBuildSend_FailReceiverNotOptedIn(t *testing.T) { Amount: 100, SenderPrivKey: senderKP.PrivKeyHex, SenderPubKey: senderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: currentBalance, High: currentBalance}, }) require.ErrorIs(t, err, ErrReceiverNotOptedIn) } diff --git a/confidential/commitment/commitment.go b/confidential/commitment/commitment.go index 964af1cd7..225895b49 100644 --- a/confidential/commitment/commitment.go +++ b/confidential/commitment/commitment.go @@ -18,8 +18,7 @@ func Create(amount uint64, bfHex string) (string, error) { return "", fmt.Errorf("%w: %w", ErrInvalidBlindingFactor, err) } - var bf [mptcrypto.BlindingFactorSize]byte - copy(bf[:], bfBytes) + bf := mptcrypto.BlindingFactor(bfBytes) c, err := mptcrypto.PedersenCommitment(amount, bf) if err != nil { diff --git a/confidential/commitment/commitment_test.go b/confidential/commitment/commitment_test.go index 7d6b5b8e6..ca0bc7a1f 100644 --- a/confidential/commitment/commitment_test.go +++ b/confidential/commitment/commitment_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package commitment_test diff --git a/confidential/elgamal/elgamal.go b/confidential/elgamal/elgamal.go index 1ca8374c9..d4e89a84e 100644 --- a/confidential/elgamal/elgamal.go +++ b/confidential/elgamal/elgamal.go @@ -6,6 +6,7 @@ package elgamal import ( "encoding/hex" "fmt" + "math" "github.com/Peersyst/xrpl-go/confidential/mptcrypto" "github.com/Peersyst/xrpl-go/pkg/hexutil" @@ -17,6 +18,23 @@ type Keypair struct { PubKeyHex string // 66 hex chars (33 bytes, compressed) } +// AmountRange defines inclusive bounds for a decryption search. +type AmountRange struct { + Low uint64 + High uint64 +} + +// Validate checks that the inclusive decryption range can be searched safely. +func (r AmountRange) Validate() error { + if r.Low > r.High { + return fmt.Errorf("%w: low %d exceeds high %d", ErrInvalidAmountRange, r.Low, r.High) + } + if r.High == math.MaxUint64 { + return fmt.Errorf("%w: high must be less than %d", ErrInvalidAmountRange, uint64(math.MaxUint64)) + } + return nil +} + // GenerateKeypair creates a new secp256k1 ElGamal keypair with hex-encoded keys. func GenerateKeypair() (Keypair, error) { priv, pub, err := mptcrypto.GenerateKeypair() @@ -51,10 +69,8 @@ func Encrypt(amount uint64, pubkeyHex, bfHex string) (string, error) { return "", fmt.Errorf("%w: %w", ErrInvalidBlindingFactor, err) } - var pub [mptcrypto.PubKeySize]byte - var bf [mptcrypto.BlindingFactorSize]byte - copy(pub[:], pubBytes) - copy(bf[:], bfBytes) + pub := mptcrypto.PublicKey(pubBytes) + bf := mptcrypto.BlindingFactor(bfBytes) ct, err := mptcrypto.EncryptAmount(amount, pub, bf) if err != nil { @@ -63,25 +79,27 @@ func Encrypt(amount uint64, pubkeyHex, bfHex string) (string, error) { return hex.EncodeToString(ct[:]), nil } -// Decrypt decrypts a ciphertext using a private key. -// ciphertextHex: 132 hex chars (66 bytes), privkeyHex: 64 hex chars (32 bytes). -// Returns the plaintext uint64 amount. -func Decrypt(ciphertextHex, privkeyHex string) (uint64, error) { +// Decrypt decrypts a ciphertext using a private key by searching amountRange. +// ciphertextHex: 132 hex chars (66 bytes), privateKeyHex: 64 hex chars (32 bytes). +// The amount range bounds are inclusive and the search cost is linear. +func Decrypt(ciphertextHex, privateKeyHex string, amountRange AmountRange) (uint64, error) { + if err := amountRange.Validate(); err != nil { + return 0, err + } + ctBytes, err := hexutil.DecodeFixedHex(ciphertextHex, mptcrypto.CiphertextSize) if err != nil { return 0, fmt.Errorf("%w: %w", ErrInvalidCiphertext, err) } - privBytes, err := hexutil.DecodeFixedHex(privkeyHex, mptcrypto.PrivKeySize) + privBytes, err := hexutil.DecodeFixedHex(privateKeyHex, mptcrypto.PrivKeySize) if err != nil { return 0, fmt.Errorf("%w: %w", ErrInvalidKey, err) } - var ct [mptcrypto.CiphertextSize]byte - var priv [mptcrypto.PrivKeySize]byte - copy(ct[:], ctBytes) - copy(priv[:], privBytes) + ct := mptcrypto.Ciphertext(ctBytes) + privateKey := mptcrypto.PrivateKey(privBytes) - result, err := mptcrypto.DecryptAmount(ct, priv) + result, err := mptcrypto.DecryptAmount(ct, privateKey, amountRange.Low, amountRange.High) if err != nil { return 0, fmt.Errorf("%w: %w", ErrDecryptFailed, err) } diff --git a/confidential/elgamal/elgamal_test.go b/confidential/elgamal/elgamal_test.go index 255f87f18..5f596ed2a 100644 --- a/confidential/elgamal/elgamal_test.go +++ b/confidential/elgamal/elgamal_test.go @@ -4,6 +4,7 @@ package elgamal_test import ( "math" + "strings" "testing" "github.com/Peersyst/xrpl-go/confidential/elgamal" @@ -46,16 +47,13 @@ func TestGenerateBlindingFactor(t *testing.T) { func TestEncryptDecryptRoundtrip(t *testing.T) { tests := []struct { - name string - amount uint64 - // skipOnDecryptErr skips the subtest instead of failing when the C - // library cannot recover the plaintext (BSGS table limitation). - skipOnDecryptErr bool + name string + amount uint64 + amountRange elgamal.AmountRange }{ - {"pass - zero", 0, false}, - {"pass - small value", 42, false}, - {"pass - one million", 1_000_000, false}, - {"pass - max uint64", math.MaxUint64, true}, + {name: "pass - zero", amount: 0, amountRange: elgamal.AmountRange{Low: 0, High: 0}}, + {name: "pass - small value", amount: 42, amountRange: elgamal.AmountRange{Low: 40, High: 50}}, + {name: "pass - one million", amount: 1_000_000, amountRange: elgamal.AmountRange{Low: 1_000_000, High: 1_000_000}}, } kp, err := elgamal.GenerateKeypair() @@ -70,16 +68,33 @@ func TestEncryptDecryptRoundtrip(t *testing.T) { require.NoError(t, err) require.Len(t, ct, mptcrypto.CiphertextSize*2) - got, err := elgamal.Decrypt(ct, kp.PrivKeyHex) - if err != nil && tt.skipOnDecryptErr { - t.Skipf("Decrypt not supported for this value: %v", err) - } + got, err := elgamal.Decrypt(ct, kp.PrivKeyHex, tt.amountRange) require.NoError(t, err) require.Equal(t, tt.amount, got) }) } } +func TestAmountRangeValidate(t *testing.T) { + tests := []struct { + name string + amountRange elgamal.AmountRange + wantErr error + }{ + {name: "pass - inclusive range", amountRange: elgamal.AmountRange{Low: 1, High: 2}}, + {name: "pass - single-value range", amountRange: elgamal.AmountRange{Low: 1, High: 1}}, + {name: "fail - low exceeds high", amountRange: elgamal.AmountRange{Low: 2, High: 1}, wantErr: elgamal.ErrInvalidAmountRange}, + {name: "fail - high is max uint64", amountRange: elgamal.AmountRange{Low: 0, High: math.MaxUint64}, wantErr: elgamal.ErrInvalidAmountRange}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.amountRange.Validate() + require.ErrorIs(t, err, tt.wantErr) + }) + } +} + func TestEncryptMultipleKeys(t *testing.T) { kp1, err := elgamal.GenerateKeypair() require.NoError(t, err) @@ -99,25 +114,41 @@ func TestEncryptMultipleKeys(t *testing.T) { require.NotEqual(t, ct1, ct2, "same amount with different keys produced identical ciphertexts") } -func TestDecryptWithWrongKey(t *testing.T) { - kp1, err := elgamal.GenerateKeypair() +func TestDecryptFailures(t *testing.T) { + kp, err := elgamal.GenerateKeypair() require.NoError(t, err) - kp2, err := elgamal.GenerateKeypair() + wrongKP, err := elgamal.GenerateKeypair() require.NoError(t, err) bf, err := elgamal.GenerateBlindingFactor() require.NoError(t, err) - ct, err := elgamal.Encrypt(42, kp1.PubKeyHex, bf) + ciphertext, err := elgamal.Encrypt(42, kp.PubKeyHex, bf) require.NoError(t, err) - // Decrypting with a different private key should fail. - _, err = elgamal.Decrypt(ct, kp2.PrivKeyHex) - require.ErrorIs(t, err, elgamal.ErrDecryptFailed) + tests := []struct { + name string + privateKey string + amountRange elgamal.AmountRange + }{ + {name: "fail - amount outside range", privateKey: kp.PrivKeyHex, amountRange: elgamal.AmountRange{Low: 0, High: 41}}, + {name: "fail - wrong private key", privateKey: wrongKP.PrivKeyHex, amountRange: elgamal.AmountRange{Low: 0, High: 100}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := elgamal.Decrypt(ciphertext, tt.privateKey, tt.amountRange) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) + }) + } } func TestInvalidHexInputs(t *testing.T) { - kp, _ := elgamal.GenerateKeypair() - bf, _ := elgamal.GenerateBlindingFactor() + kp, err := elgamal.GenerateKeypair() + require.NoError(t, err) + bf, err := elgamal.GenerateBlindingFactor() + require.NoError(t, err) + ciphertext, err := elgamal.Encrypt(1, kp.PubKeyHex, bf) + require.NoError(t, err) tests := []struct { name string @@ -151,7 +182,23 @@ func TestInvalidHexInputs(t *testing.T) { { name: "fail - decrypt bad ciphertext", fn: func() error { - _, err := elgamal.Decrypt("zz", kp.PrivKeyHex) + _, err := elgamal.Decrypt("zz", kp.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidCiphertext, + }, + { + name: "fail - decrypt short ciphertext", + fn: func() error { + _, err := elgamal.Decrypt(strings.Repeat("00", mptcrypto.CiphertextSize-1), kp.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidCiphertext, + }, + { + name: "fail - decrypt long ciphertext", + fn: func() error { + _, err := elgamal.Decrypt(strings.Repeat("00", mptcrypto.CiphertextSize+1), kp.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 1}) return err }, wantErr: elgamal.ErrInvalidCiphertext, @@ -159,17 +206,39 @@ func TestInvalidHexInputs(t *testing.T) { { name: "fail - decrypt bad privkey", fn: func() error { - ct, _ := elgamal.Encrypt(1, kp.PubKeyHex, bf) - _, err := elgamal.Decrypt(ct, "short") + _, err := elgamal.Decrypt(ciphertext, "short", elgamal.AmountRange{Low: 0, High: 1}) return err }, wantErr: elgamal.ErrInvalidKey, }, + { + name: "fail - decrypt short privkey", + fn: func() error { + _, err := elgamal.Decrypt(ciphertext, strings.Repeat("00", mptcrypto.PrivKeySize-1), elgamal.AmountRange{Low: 0, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidKey, + }, + { + name: "fail - decrypt long privkey", + fn: func() error { + _, err := elgamal.Decrypt(ciphertext, strings.Repeat("00", mptcrypto.PrivKeySize+1), elgamal.AmountRange{Low: 0, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidKey, + }, + { + name: "fail - decrypt invalid range before decoding inputs", + fn: func() error { + _, err := elgamal.Decrypt("zz", "short", elgamal.AmountRange{Low: 2, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidAmountRange, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - err := tc.fn() - require.ErrorIs(t, err, tc.wantErr) + require.ErrorIs(t, tc.fn(), tc.wantErr) }) } } diff --git a/confidential/elgamal/errors.go b/confidential/elgamal/errors.go index 685142a08..60cf51cc0 100644 --- a/confidential/elgamal/errors.go +++ b/confidential/elgamal/errors.go @@ -13,4 +13,6 @@ var ( ErrEncryptFailed = errors.New("elgamal: encryption failed") // ErrDecryptFailed is returned when the underlying C decryption call fails. ErrDecryptFailed = errors.New("elgamal: decryption failed") + // ErrInvalidAmountRange is returned when a decryption search range is invalid. + ErrInvalidAmountRange = errors.New("elgamal: invalid amount range") ) diff --git a/confidential/mptcrypto/README.md b/confidential/mptcrypto/README.md index e58bbf814..b383b0e60 100644 --- a/confidential/mptcrypto/README.md +++ b/confidential/mptcrypto/README.md @@ -1,350 +1,307 @@ # mptcrypto -Go bindings for the [XRPLF/mpt-crypto](https://github.com/xrplf/mpt-crypto) C library. This package is the **only** place in the codebase that imports `"C"` (CGo). Everything above this layer (elgamal/, proof/, commitment/) is pure Go. +`mptcrypto` is the low-level Go binding for the [XRPLF/mpt-crypto](https://github.com/XRPLF/mpt-crypto) C library used by XLS-96 Confidential MPT Transfers. It exposes EC-ElGamal encryption, Pedersen commitments, transaction context hashes, and the proof generation and verification routines required by confidential MPT transactions. -## Build requirements +This is the only package in this repository that imports `"C"`. Higher-level packages such as `confidential/elgamal`, `confidential/commitment`, and `confidential/proof` are pure Go wrappers that handle hex encoding, address decoding, and domain-specific errors. -CGo must be enabled (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. If you build without CGo, every function returns `ErrCgoRequired`. +## Native backend availability -```bash -# normal build (CGo on by default) -go test ./confidential/mptcrypto/... - -# force CGo off (all functions return ErrCgoRequired) -CGO_ENABLED=0 go test ./confidential/mptcrypto/... -``` - -## How this package is organized - -``` -mptcrypto/ - types.go # Size constants, Participant, PedersenProofParams - mptcrypto_cgo.go # Real implementations (only built with CGo) - mptcrypto_nocgo.go # Stubs that return ErrCgoRequired (built without CGo) - mptcrypto_test.go # Tests (build tag: cgo) -``` +The native implementation is selected only when all of the following are true: -Every function works with **fixed-size byte arrays** (`[32]byte`, `[33]byte`, `[66]byte`, etc.). Hex encoding/decoding happens in the layers above (elgamal/, proofs/, commitment/), never here. +- cgo is enabled (`CGO_ENABLED=1`) +- the target OS is Linux or macOS (`darwin`) +- the target architecture is `amd64` or `arm64` +- the build is not targeting `js`, `wasip1`, TinyGo, or go-fuzz ---- +Building the native implementation also requires the C/C++ compiler and linker toolchain used by cgo (for example, `gcc`/`g++` on Linux or the Xcode command-line tools on macOS). Enabling cgo alone does not install that toolchain. -## Types and constants +Vendored headers and static libraries live under `confidential/deps/`: -### Size constants +| Target | Library directory | +| --- | --- | +| Linux amd64 | `confidential/deps/libs/linux-amd64/` | +| Linux arm64 | `confidential/deps/libs/linux-arm64/` | +| macOS amd64 | `confidential/deps/libs/darwin-amd64/` | +| macOS arm64 | `confidential/deps/libs/darwin-arm64/` | -| Constant | Bytes | What it is | -|---|---|---| -| `PrivKeySize` | 32 | secp256k1 private key | -| `PubKeySize` | 33 | Compressed secp256k1 public key | -| `BlindingFactorSize` | 32 | Random scalar for encryption/commitment | -| `CiphertextSize` | 66 | ElGamal ciphertext (two compressed points: C1 || C2) | -| `AccountIDSize` | 20 | XRPL account ID (decoded from classic address) | -| `IssuanceIDSize` | 24 | MPTokenIssuance ID | -| `HashOutputSize` | 32 | Context hash output (half-SHA) | -| `CommitmentSize` | 33 | Compressed Pedersen commitment point | -| `SchnorrProofSize` | 64 | Schnorr proof of knowledge | -| `SingleBulletproofSize` | 688 | Single bulletproof (range proof for 1 value) | -| `DoubleBulletproofSize` | 754 | Double bulletproof (range proof for 2 values) | -| `CompactClawbackProofSize` | 64 | Compact sigma proof for clawback | -| `CompactConvertBackProofSize` | 128 | Compact sigma proof for convert-back | -| `CompactSendProofSize` | 192 | Compact sigma proof for send | -| `ConvertBackProofSize` | 816 | Compact sigma + single bulletproof (128 + 688) | -| `SendProofSize` | 946 | Compact sigma + double bulletproof (192 + 754) | -| `MaxParticipants` | 255 | Max participants in a send (C API uses uint8_t) | - -### Structs +All other builds select `mptcrypto_nocgo.go`. The package still compiles and exposes the same API, but every operation immediately returns `ErrCgoRequired` without validating or processing its inputs. This includes builds with cgo enabled on an unsupported OS or architecture. -```go -// A party in a confidential send (public key + their encrypted amount). -type Participant struct { - PubKey [PubKeySize]byte // 33 bytes - Ciphertext [CiphertextSize]byte // 66 bytes -} +```bash +# Exercise the native implementation on a supported host. +go test ./confidential/mptcrypto -// Parameters for generating Pedersen linkage proofs. -type PedersenProofParams struct { - Commitment [CommitmentSize]byte // 33 bytes - Amount uint64 - Ciphertext [CiphertextSize]byte // 66 bytes - BlindingFactor [BlindingFactorSize]byte // 32 bytes -} +# Exercise the fallback implementation. +CGO_ENABLED=0 go test ./confidential/mptcrypto ``` ---- - -## Function reference +## Package layout -### 1. ElGamal encryption +```text +mptcrypto/ + types.go # Package documentation, sizes, and value types + errors.go # Shared sentinel errors + mptcrypto_cgo.go # Native bindings and native-only validation + mptcrypto_nocgo.go # Unavailable-backend stubs + mptcrypto_test.go # Native cryptographic tests + mptcrypto_nocgo_test.go # Fallback availability contract +``` -These handle key generation, encryption, and decryption for confidential amounts. +## Data model -#### `GenerateKeypair() (privkey [32]byte, pubkey [33]byte, err error)` +### Size constants -Creates a new secp256k1 ElGamal keypair. +All sizes are in bytes and match `confidential/deps/include/utility/mpt_utility.h`. + +| Constant | Bytes | Meaning | +| --- | ---: | --- | +| `PrivKeySize` | 32 | ElGamal private key | +| `PubKeySize` | 33 | Compressed secp256k1 ElGamal public key | +| `BlindingFactorSize` | 32 | ElGamal randomness / Pedersen blinding scalar | +| `CiphertextSize` | 66 | Two compressed EC points (`C1 || C2`) | +| `AccountIDSize` | 20 | Decoded XRPL account ID | +| `IssuanceIDSize` | 24 | MPToken issuance ID | +| `HashOutputSize` | 32 | Transaction context hash | +| `CommitmentSize` | 33 | Compressed Pedersen commitment | +| `SchnorrProofSize` | 64 | Convert Schnorr proof | +| `SingleBulletproofSize` | 688 | Range proof for one value | +| `DoubleBulletproofSize` | 754 | Aggregated range proof for two values | +| `CompactClawbackProofSize` | 64 | Clawback compact sigma proof | +| `CompactConvertBackProofSize` | 128 | Convert-back compact sigma proof | +| `CompactSendProofSize` | 192 | Send compact sigma proof | +| `ConvertBackProofSize` | 816 | `128 + 688` bytes | +| `SendProofSize` | 946 | `192 + 754` bytes | +| `MaxParticipants` | 255 | Maximum representable participant count in the verification C API | + +### Defined byte-array types + +The main cryptographic values use distinct fixed-size types: ```go -priv, pub, err := mptcrypto.GenerateKeypair() -// priv: 32-byte private key -// pub: 33-byte compressed public key (starts with 0x02 or 0x03) +type PrivateKey [PrivKeySize]byte +type PublicKey [PubKeySize]byte +type BlindingFactor [BlindingFactorSize]byte +type Ciphertext [CiphertextSize]byte +type Commitment [CommitmentSize]byte +type ContextHash [HashOutputSize]byte ``` -#### `GenerateBlindingFactor() (bf [32]byte, err error)` - -Generates a cryptographically random 32-byte scalar. Used as the randomness parameter (`r`) when encrypting amounts or creating Pedersen commitments. - -```go -bf, err := mptcrypto.GenerateBlindingFactor() -``` +These types prevent accidental substitutions between same-sized values. Account IDs and issuance IDs are accepted as `[AccountIDSize]byte` and `[IssuanceIDSize]byte`; proof parameters use fixed-size arrays except for a full send proof, which crosses the API as `[]byte` and is length-checked by `VerifySendProof`. -#### `EncryptAmount(amount uint64, pubkey [33]byte, bf [32]byte) (ct [66]byte, err error)` +The Go types enforce byte lengths, not cryptographic validity. The native library validates private scalars, public keys, curve points, ciphertexts, commitments, and proofs when performing an operation. -Encrypts an amount using ElGamal. The ciphertext is 66 bytes: two compressed EC points concatenated (C1 || C2). +### Compound inputs ```go -ct, err := mptcrypto.EncryptAmount(1000, pubkey, blindingFactor) -// ct: 66-byte ciphertext -``` - -#### `DecryptAmount(ciphertext [66]byte, privkey [32]byte) (uint64, error)` - -Decrypts an ElGamal ciphertext back to the original amount. Uses a baby-step giant-step (BSGS) lookup table internally, so very large values (close to `math.MaxUint64`) may fail to decrypt. +// One encrypted copy of a confidential send amount. +type Participant struct { + PubKey PublicKey + Ciphertext Ciphertext +} -```go -amount, err := mptcrypto.DecryptAmount(ciphertext, privkey) +// A value represented by both an ElGamal ciphertext and a Pedersen commitment. +type PedersenProofParams struct { + Commitment Commitment + Amount uint64 + Ciphertext Ciphertext + BlindingFactor BlindingFactor +} ``` -### 2. Context hashes - -Every ZK proof is bound to a specific transaction via a **context hash**. This prevents proof reuse across transactions. Each transaction type has its own hash function because the inputs differ. +For send proofs, XLS-96 uses three participants, or four when an auditor is configured. Their order is part of the native proof contract: -All context hash functions return a `[32]byte` hash. +1. sender +2. destination +3. issuer +4. optional auditor -#### `ConvertContextHash(account [20]byte, iss [24]byte, seq uint32) ([32]byte, error)` +Each participant ciphertext must encrypt the transfer amount under that participant's public key using the same transaction blinding factor. The Go wrapper rejects an empty list or more than `MaxParticipants`; the underlying native routine defines the valid XLS-96 count as three or four. -For **ConfidentialMPTConvert** transactions (public amount -> confidential). +## Function reference -- `account`: the sender's 20-byte account ID -- `iss`: the 24-byte MPTokenIssuance ID -- `seq`: the transaction sequence number +### ElGamal -#### `ConvertBackContextHash(account [20]byte, iss [24]byte, seq, ver uint32) ([32]byte, error)` +#### `GenerateKeypair() (PrivateKey, PublicKey, error)` -For **ConfidentialMPTConvertBack** transactions (confidential -> public amount). +Generates a secp256k1 ElGamal keypair. The public key is a 33-byte compressed point. -Same as above plus `ver` (the version counter from the ledger object). +#### `GenerateBlindingFactor() (BlindingFactor, error)` -#### `SendContextHash(account [20]byte, iss [24]byte, seq uint32, dest [20]byte, ver uint32) ([32]byte, error)` +Generates a random scalar suitable for ElGamal encryption and Pedersen commitments. -For **ConfidentialMPTSend** transactions (confidential transfer between accounts). +#### `EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (Ciphertext, error)` -Adds `dest` (destination account ID) and `ver`. +Encrypts `amount` under `pubkey` using `bf`. The result is the concatenation of two compressed EC points. -#### `ClawbackContextHash(account [20]byte, iss [24]byte, seq uint32, holder [20]byte) ([32]byte, error)` +#### `DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, rangeHigh uint64) (uint64, error)` -For **ConfidentialMPTClawback** transactions (issuer reclaims tokens from a holder). +Searches for the plaintext in the inclusive interval `[rangeLow, rangeHigh]`. On a native build, the range must satisfy: -Adds `holder` (the account being clawed back from). +```text +rangeLow <= rangeHigh < math.MaxUint64 +``` -### 3. Pedersen commitment +Invalid ranges wrap `ErrInvalidAmountRange`. Decryption cost grows linearly with the interval width, so callers should use the narrowest practical range. If the native backend is unavailable, `ErrCgoRequired` is returned before range validation. -#### `PedersenCommitment(amount uint64, bf [32]byte) (commitment [33]byte, err error)` +### Transaction context hashes -Computes `C = amount*G + bf*H` where G and H are generator points. The result is a 33-byte compressed point. Two commitments with the same amount and blinding factor always produce the same output (deterministic). +Context hashes bind proofs to transaction-specific fields. All helpers return `ContextHash`. ```go -commitment, err := mptcrypto.PedersenCommitment(1000, blindingFactor) -// commitment: 33-byte compressed point (starts with 0x02 or 0x03) +func ConvertContextHash( + account [AccountIDSize]byte, + iss [IssuanceIDSize]byte, + seq uint32, +) (ContextHash, error) + +func ConvertBackContextHash( + account [AccountIDSize]byte, + iss [IssuanceIDSize]byte, + seq, ver uint32, +) (ContextHash, error) + +func SendContextHash( + account [AccountIDSize]byte, + iss [IssuanceIDSize]byte, + seq uint32, + dest [AccountIDSize]byte, + ver uint32, +) (ContextHash, error) + +func ClawbackContextHash( + account [AccountIDSize]byte, + iss [IssuanceIDSize]byte, + seq uint32, + holder [AccountIDSize]byte, +) (ContextHash, error) ``` -### 4. Proof generation - -Each XRPL confidential transaction type requires a specific proof. The proof convinces validators that the transaction is valid without revealing the actual amounts. - -#### `GenerateConvertProof(pubkey [33]byte, privkey [32]byte, ctxHash [32]byte) ([64]byte, error)` +The fields correspond to the relevant XLS-96 transaction: -**Schnorr proof of knowledge.** Proves you own the private key for the public key being registered, bound to the transaction via ctxHash. +- convert: holder account, issuance ID, and transaction sequence +- convert back: the same fields plus the holder's confidential balance version +- send: sender, destination, issuance ID, sequence, and sender balance version +- clawback: issuer account, target holder, issuance ID, and sequence -Used in: **ConfidentialMPTConvert** (registering a keypair on the ledger). +### Pedersen commitments -#### `GenerateConvertBackProof(privkey [32]byte, pubkey [33]byte, ctxHash [32]byte, amount uint64, params PedersenProofParams) ([816]byte, error)` +#### `PedersenCommitment(amount uint64, bf BlindingFactor) (Commitment, error)` -**Compact AND-composed sigma proof + single Bulletproof range proof.** Proves: -1. Your encrypted balance matches the Pedersen commitment (sigma proof over balance witness) -2. After subtracting the convert-back amount, the remaining balance is non-negative (range proof over remainder commitment) +Computes a compressed Pedersen commitment to `amount` using `bf`. The operation is deterministic for the same amount and blinding factor. -Used in: **ConfidentialMPTConvertBack**. +#### `ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (Commitment, error)` -#### `GenerateClawbackProof(privkey [32]byte, pubkey [33]byte, ctxHash [32]byte, amount uint64, ciphertext [66]byte) ([64]byte, error)` +Subtracts the transparent amount from a balance commitment and returns the commitment to the convert-back remainder. -**Compact sigma proof.** Proves that the ciphertext decrypts to exactly the claimed amount, without revealing the private key. +### Proof generation -Used in: **ConfidentialMPTClawback** (issuer proves the amount they're clawing back matches the encrypted balance). +#### `GenerateConvertProof(pubkey PublicKey, privkey PrivateKey, ctxHash ContextHash) ([SchnorrProofSize]byte, error)` -#### `GenerateSendProof(privkey [32]byte, pubkey [33]byte, amount uint64, participants []Participant, txBF [32]byte, ctxHash [32]byte, amountCommitment [33]byte, balanceParams PedersenProofParams) ([]byte, error)` +Generates the Schnorr proof of private-key knowledge used when a `ConfidentialMPTConvert` transaction registers a holder encryption key. -**Compact AND-composed sigma proof + aggregated Bulletproof range proof** (the most complex one). Combines: -1. **Equality proof** - same amount encrypted for sender, receiver, issuer (and optionally auditor) -2. **Amount linkage** - ElGamal ciphertext matches amount commitment -3. **Balance linkage** - sender's encrypted balance matches balance commitment -4. **Range proof** - amount and remaining balance are both in [0, 2^64-1] +#### `GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) ([ConvertBackProofSize]byte, error)` -Returns a fixed-size byte slice of `SendProofSize` (946) bytes. +Generates an 816-byte proof containing: -Used in: **ConfidentialMPTSend**. +- a 128-byte compact sigma proof binding the holder key, encrypted spending balance, and balance commitment +- a 688-byte range proof showing that the balance remaining after `amount` is subtracted is non-negative -### 5. Proof verification (top-level) +`params` describes the holder's original spending balance, ciphertext, commitment, and commitment blinding factor. -These are the four main verifiers, one per transaction type. Each returns `nil` on success or an error on failure. +#### `GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) ([CompactClawbackProofSize]byte, error)` -#### `VerifyConvertProof(proof [64]byte, pubkey [33]byte, ctxHash [32]byte) error` +Generates the 64-byte compact sigma proof used by `ConfidentialMPTClawback`. It proves that the issuer-encrypted balance ciphertext contains the revealed clawback amount without exposing the issuer private key. -Verifies the Schnorr proof from a ConfidentialMPTConvert. +#### `GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error)` -#### `VerifyConvertBackProof(proof [816]byte, pubkey [33]byte, ciphertext [66]byte, balanceCommit [33]byte, amount uint64, ctxHash [32]byte) error` +Generates the 946-byte `ConfidentialMPTSend` proof: -Verifies the compact sigma + range proof from a ConfidentialMPTConvertBack. +- 192-byte compact sigma proof for ciphertext consistency, amount linkage, balance linkage, and sender key ownership +- 754-byte aggregated range proof for the transfer amount and post-send balance -#### `VerifySendProof(proof []byte, participants []Participant, senderCt [66]byte, amountCommit, balanceCommit [33]byte, ctxHash [32]byte) error` +The inputs have the following relationships: -Verifies the compact sigma + range proof from a ConfidentialMPTSend. +- `privkey` and `pubkey` are the sender's keypair. +- `participants` follows the sender, destination, issuer, optional-auditor order described above. +- Every participant ciphertext encrypts `amount` with `txBF`. +- `amountCommitment` must be the commitment returned by `PedersenCommitment(amount, txBF)`; it intentionally reuses the ElGamal randomness. +- `balanceParams` describes the sender's original spending balance and its commitment witness. -#### `VerifyClawbackProof(proof [64]byte, amount uint64, pubkey [33]byte, ciphertext [66]byte, ctxHash [32]byte) error` +The successful native call currently writes `SendProofSize` bytes. The slice return type mirrors the C API's output buffer plus output-length contract. -Verifies the compact sigma proof from a ConfidentialMPTClawback. - -### 6. Proof verification (internal components) - -These verify individual pieces of a send proof. Useful for debugging or testing each component in isolation. - -#### `VerifyRevealedAmount(amount uint64, bf [32]byte, holder, issuer Participant, auditor *Participant) error` - -Verifies that a plaintext amount and blinding factor are consistent with the participants' ciphertexts. `auditor` can be `nil` if there's no auditor. - -#### `VerifySendRangeProof(proof [754]byte, amountCommit, balanceCommitment [33]byte, ctxHash [32]byte) error` - -Verifies a double bulletproof: both the transfer amount and remaining balance are in [0, 2^64-1]. - -### 7. Utilities - -#### `ComputeConvertBackRemainder(commitmentIn [33]byte, amount uint64) ([33]byte, error)` - -Subtracts a transparent (public) amount from a hidden Pedersen commitment, producing a new commitment for the remaining balance. Used in convert-back to compute the post-transaction balance commitment. +### Top-level verification ```go -remainder, err := mptcrypto.ComputeConvertBackRemainder(balanceCommitment, 500) +func VerifyConvertProof( + proof [SchnorrProofSize]byte, + pubkey PublicKey, + ctxHash ContextHash, +) error + +func VerifyConvertBackProof( + proof [ConvertBackProofSize]byte, + pubkey PublicKey, + ciphertext Ciphertext, + balanceCommit Commitment, + amount uint64, + ctxHash ContextHash, +) error + +func VerifySendProof( + proof []byte, + participants []Participant, + senderCt Ciphertext, + amountCommit, balanceCommit Commitment, + ctxHash ContextHash, +) error + +func VerifyClawbackProof( + proof [CompactClawbackProofSize]byte, + amount uint64, + pubkey PublicKey, + ciphertext Ciphertext, + ctxHash ContextHash, +) error ``` ---- - -## CGo patterns used in this package - -If you need to modify or extend the bindings, here's how the CGo boundary works. - -### The preamble - -At the top of `mptcrypto_cgo.go`: - -```go -/* -#cgo CFLAGS: -I${SRCDIR}/../deps/include -I${SRCDIR}/../deps/include/utility -#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../deps/libs/linux-amd64 -lmpt-crypto -lsecp256k1 ... - -#include "mpt_utility.h" -*/ -import "C" -``` - -The comment block before `import "C"` is special: it's the **CGo preamble**. `#cgo` directives set compiler/linker flags per platform. `#include` pulls in the C header. `import "C"` must appear immediately after the comment (no blank line). - -### Passing byte arrays to C with unsafe.Pointer +Each verifier returns `nil` only when the native proof check succeeds. Additional input contracts: -The C functions expect raw `uint8_t*` pointers. Go arrays live in Go-managed memory, so we take the address of the first element and cast: +- `VerifyConvertBackProof` expects the original balance commitment. The native library subtracts `amount` before verifying the remainder range proof; do not pass a precomputed remainder commitment. +- `VerifySendProof` requires exactly `SendProofSize` proof bytes and the same ordered participant list used for generation. `senderCt` is the sender's original on-ledger spending-balance ciphertext, while the participant ciphertexts encrypt the transfer amount. +- `VerifySendProof` expects the original amount and balance commitments used to generate the proof. -```go -// Go side -var pubkey [33]byte - -// Pass to C: "give me a *C.uint8_t pointing to pubkey[0]" -C.some_c_function((*C.uint8_t)(unsafe.Pointer(&pubkey[0]))) -``` +### Auxiliary verification -**What's happening step by step:** +#### `VerifyRevealedAmount(amount uint64, bf BlindingFactor, holder, issuer Participant, auditor *Participant) error` -1. `&pubkey[0]` - address of the first byte (type `*byte`) -2. `unsafe.Pointer(...)` - convert to an untyped pointer (required bridge between Go and C pointer types) -3. `(*C.uint8_t)(...)` - cast to the C type the function expects +Checks deterministically that the holder, issuer, and optional auditor ciphertexts all encrypt the revealed `amount` using `bf`. This is a direct plaintext/ciphertext consistency check, not a ZK-proof verifier. Pass `nil` when no auditor ciphertext is required. -This is safe because: -- Go arrays are contiguous in memory, just like C arrays -- The C function only reads/writes within the declared size -- The Go array stays alive for the duration of the C call (it's on the stack or referenced) +#### `VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) error` -### Converting Go structs to C structs +Verifies the 754-byte aggregated range-proof component from a send proof. `balanceCommitment` must be the sender's original balance commitment; the native library derives the post-send remainder from it and `amountCommit`. Do not pass a precomputed remainder commitment. -For complex types (account IDs, participants, proof params), we use helper functions that copy field-by-field: - -```go -func toParticipant(p Participant) C.mpt_confidential_participant { - var c C.mpt_confidential_participant - for i, b := range p.PubKey { - c.pubkey[i] = C.uint8_t(b) - } - for i, b := range p.Ciphertext { - c.ciphertext[i] = C.uint8_t(b) - } - return c -} -``` +## Error behavior -We copy byte-by-byte instead of using `unsafe` casts on structs because Go and C may have different struct layouts (padding, alignment). Byte-by-byte copy is always correct. +The package exposes two sentinel errors: -### Passing slices to C (variable-length data) +- `ErrCgoRequired`: the native backend is unavailable for the current build. +- `ErrInvalidAmountRange`: a native `DecryptAmount` call received invalid search bounds. -For `GenerateSendProof` and similar functions that take a variable number of participants: +Use `errors.Is` for these sentinels because range errors include bound details: ```go -cParts := make([]C.mpt_confidential_participant, n) -for i, p := range participants { - cParts[i] = toParticipant(p) +amount, err := mptcrypto.DecryptAmount(ciphertext, privateKey, low, high) +if errors.Is(err, mptcrypto.ErrCgoRequired) { + // Confidential cryptography is unavailable in this build. } - -// Pass the slice's backing array to C -C.mpt_get_confidential_send_proof( - // ... - &cParts[0], // pointer to first element - C.size_t(n), // length - // ... -) -``` - -Go slices have a backing array that's contiguous, so `&cParts[0]` gives C a valid pointer to `n` consecutive structs. - -### Optional (nullable) pointers - -Some C functions accept `NULL` for optional parameters (e.g., auditor in `VerifyRevealedAmount`): - -```go -var cAuditor *C.mpt_confidential_participant // nil by default (maps to NULL) -if auditor != nil { - a := toParticipant(*auditor) - cAuditor = &a +if errors.Is(err, mptcrypto.ErrInvalidAmountRange) { + // Fix the caller-supplied range. } -C.mpt_verify_revealed_amount(..., cAuditor) ``` -A nil Go pointer becomes `NULL` in C. - -### Error handling +Other validation and native-library failures are returned as descriptive errors. Every native wrapper treats a non-zero C return code as failure. -All C functions return `int`: 0 for success, -1 for failure. The Go wrappers turn non-zero returns into errors: - -```go -ret := C.mpt_some_function(...) -if ret != 0 { - return fmt.Errorf("mpt_some_function failed with code %d", ret) -} -``` +## Maintaining the cgo boundary -### The no-CGo build +`mptcrypto_cgo.go` contains the build constraint and per-platform linker flags. It passes fixed-size byte arrays to C through pointers to their first elements and copies Go compound values into their corresponding C structs field by field. Variable participant lists are copied into a contiguous slice of `C.mpt_confidential_participant` values before the native call. -`mptcrypto_nocgo.go` has the build tag `//go:build !cgo`. It provides identical function signatures but every function returns `ErrCgoRequired`. This lets the rest of the codebase compile and run tests for pure-Go packages even without the C library. +The native routines use these pointers only for the duration of the call; they must not retain Go memory after returning. Keep all `import "C"`, `unsafe`, C layout conversion, and native linker changes inside this package so the higher-level confidential packages remain portable pure Go code. diff --git a/confidential/mptcrypto/errors.go b/confidential/mptcrypto/errors.go new file mode 100644 index 000000000..eaddb8a1e --- /dev/null +++ b/confidential/mptcrypto/errors.go @@ -0,0 +1,13 @@ +package mptcrypto + +import "errors" + +var ( + // ErrCgoRequired is returned when native confidential MPT operations are unavailable. + ErrCgoRequired = errors.New( + "mptcrypto: CGo is required for confidential MPT operations; " + + "rebuild with CGO_ENABLED=1 and vendored mpt-crypto libraries", + ) + // ErrInvalidAmountRange is returned when a decryption search range is invalid. + ErrInvalidAmountRange = errors.New("mptcrypto: invalid amount range") +) diff --git a/confidential/mptcrypto/mptcrypto_cgo.go b/confidential/mptcrypto/mptcrypto_cgo.go index 7d9f58bc4..b9c13a4d1 100644 --- a/confidential/mptcrypto/mptcrypto_cgo.go +++ b/confidential/mptcrypto/mptcrypto_cgo.go @@ -15,6 +15,7 @@ import "C" import ( "fmt" + "math" "unsafe" ) @@ -71,7 +72,7 @@ func toProofParams(p PedersenProofParams) C.mpt_pedersen_proof_params { // GenerateKeypair creates a new secp256k1 ElGamal keypair. // Returns a 32-byte private key and a 33-byte compressed public key. -func GenerateKeypair() (privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, err error) { +func GenerateKeypair() (privkey PrivateKey, pubkey PublicKey, err error) { ret := C.mpt_generate_keypair( uint8Ptr(&privkey[0]), uint8Ptr(&pubkey[0]), @@ -83,7 +84,7 @@ func GenerateKeypair() (privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, err } // GenerateBlindingFactor returns a random 32-byte scalar suitable for ElGamal encryption. -func GenerateBlindingFactor() (bf [BlindingFactorSize]byte, err error) { +func GenerateBlindingFactor() (bf BlindingFactor, err error) { ret := C.mpt_generate_blinding_factor( uint8Ptr(&bf[0]), ) @@ -95,7 +96,7 @@ func GenerateBlindingFactor() (bf [BlindingFactorSize]byte, err error) { // EncryptAmount encrypts a uint64 amount under a compressed public key using a blinding factor. // Returns a 66-byte ciphertext (two compressed EC points: C1 || C2). -func EncryptAmount(amount uint64, pubkey [PubKeySize]byte, bf [BlindingFactorSize]byte) (ct [CiphertextSize]byte, err error) { +func EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (ct Ciphertext, err error) { ret := C.mpt_encrypt_amount( C.uint64_t(amount), uint8Ptr(&pubkey[0]), @@ -109,13 +110,19 @@ func EncryptAmount(amount uint64, pubkey [PubKeySize]byte, bf [BlindingFactorSiz } // DecryptAmount decrypts a 66-byte ElGamal ciphertext using a private key. -// Returns the plaintext uint64 amount. -func DecryptAmount(ciphertext [CiphertextSize]byte, privkey [PrivKeySize]byte) (uint64, error) { +// It searches the inclusive [rangeLow, rangeHigh] interval with linear cost. +func DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, rangeHigh uint64) (uint64, error) { + if err := validateAmountRange(rangeLow, rangeHigh); err != nil { + return 0, err + } + var amount C.uint64_t ret := C.mpt_decrypt_amount( uint8Ptr(&ciphertext[0]), - uint8Ptr(&privkey[0]), + uint8Ptr(&privateKey[0]), &amount, + C.uint64_t(rangeLow), + C.uint64_t(rangeHigh), ) if ret != 0 { return 0, fmt.Errorf("mpt_decrypt_amount failed with code %d", ret) @@ -123,12 +130,22 @@ func DecryptAmount(ciphertext [CiphertextSize]byte, privkey [PrivKeySize]byte) ( return uint64(amount), nil } +func validateAmountRange(rangeLow, rangeHigh uint64) error { + if rangeLow > rangeHigh { + return fmt.Errorf("%w: low %d exceeds high %d", ErrInvalidAmountRange, rangeLow, rangeHigh) + } + if rangeHigh == math.MaxUint64 { + return fmt.Errorf("%w: high must be less than %d", ErrInvalidAmountRange, uint64(math.MaxUint64)) + } + return nil +} + // endregion // region Context hashes // ConvertContextHash computes the context hash for a ConfidentialMPTConvert transaction. -func ConvertContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32) (hash [HashOutputSize]byte, err error) { +func ConvertContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32) (hash ContextHash, err error) { ret := C.mpt_get_convert_context_hash( toAccountID(account), toIssuanceID(iss), @@ -142,7 +159,7 @@ func ConvertContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, s } // ConvertBackContextHash computes the context hash for a ConfidentialMPTConvertBack transaction. -func ConvertBackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq, ver uint32) (hash [HashOutputSize]byte, err error) { +func ConvertBackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq, ver uint32) (hash ContextHash, err error) { ret := C.mpt_get_convert_back_context_hash( toAccountID(account), toIssuanceID(iss), @@ -157,7 +174,7 @@ func ConvertBackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byt } // SendContextHash computes the context hash for a ConfidentialMPTSend transaction. -func SendContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, dest [AccountIDSize]byte, ver uint32) (hash [HashOutputSize]byte, err error) { +func SendContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, dest [AccountIDSize]byte, ver uint32) (hash ContextHash, err error) { ret := C.mpt_get_send_context_hash( toAccountID(account), toIssuanceID(iss), @@ -173,7 +190,7 @@ func SendContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq } // ClawbackContextHash computes the context hash for a ConfidentialMPTClawback transaction. -func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, holder [AccountIDSize]byte) (hash [HashOutputSize]byte, err error) { +func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, holder [AccountIDSize]byte) (hash ContextHash, err error) { ret := C.mpt_get_clawback_context_hash( toAccountID(account), toIssuanceID(iss), @@ -192,7 +209,7 @@ func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, // region Pedersen commitment // PedersenCommitment computes a Pedersen commitment for the given amount and blinding factor. -func PedersenCommitment(amount uint64, bf [BlindingFactorSize]byte) (commitment [CommitmentSize]byte, err error) { +func PedersenCommitment(amount uint64, bf BlindingFactor) (commitment Commitment, err error) { ret := C.mpt_get_pedersen_commitment( C.uint64_t(amount), uint8Ptr(&bf[0]), @@ -209,7 +226,7 @@ func PedersenCommitment(amount uint64, bf [BlindingFactorSize]byte) (commitment // region Proof generation // GenerateConvertProof generates a Schnorr proof of knowledge for a ConfidentialMPTConvert transaction. -func GenerateConvertProof(pubkey [PubKeySize]byte, privkey [PrivKeySize]byte, ctxHash [HashOutputSize]byte) (proof [SchnorrProofSize]byte, err error) { +func GenerateConvertProof(pubkey PublicKey, privkey PrivateKey, ctxHash ContextHash) (proof [SchnorrProofSize]byte, err error) { ret := C.mpt_get_convert_proof( uint8Ptr(&pubkey[0]), uint8Ptr(&privkey[0]), @@ -225,7 +242,7 @@ func GenerateConvertProof(pubkey [PubKeySize]byte, privkey [PrivKeySize]byte, ct // GenerateConvertBackProof generates a compact AND-composed sigma proof over the balance // witness, followed by a single Bulletproof range proof over the remainder commitment, // for a ConfidentialMPTConvertBack transaction. -func GenerateConvertBackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte, amount uint64, params PedersenProofParams) (proof [ConvertBackProofSize]byte, err error) { +func GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) (proof [ConvertBackProofSize]byte, err error) { cParams := toProofParams(params) ret := C.mpt_get_convert_back_proof( uint8Ptr(&privkey[0]), @@ -242,7 +259,7 @@ func GenerateConvertBackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte } // GenerateClawbackProof generates an equality proof for a ConfidentialMPTClawback transaction. -func GenerateClawbackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte, amount uint64, ciphertext [CiphertextSize]byte) (proof [CompactClawbackProofSize]byte, err error) { +func GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) (proof [CompactClawbackProofSize]byte, err error) { ret := C.mpt_get_clawback_proof( uint8Ptr(&privkey[0]), uint8Ptr(&pubkey[0]), @@ -259,7 +276,7 @@ func GenerateClawbackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, c // GenerateSendProof generates a compact AND-composed sigma proof + aggregated Bulletproof range proof // for a ConfidentialMPTSend transaction. -func GenerateSendProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, amount uint64, participants []Participant, txBF [BlindingFactorSize]byte, ctxHash [HashOutputSize]byte, amountCommitment [CommitmentSize]byte, balanceParams PedersenProofParams) ([]byte, error) { +func GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error) { n := len(participants) if n == 0 { return nil, fmt.Errorf("mptcrypto: at least one participant is required") @@ -301,7 +318,7 @@ func GenerateSendProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, amoun // region Proof verification (top-level) // VerifyConvertProof verifies a Schnorr proof for a ConfidentialMPTConvert transaction. -func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte) error { +func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey PublicKey, ctxHash ContextHash) error { ret := C.mpt_verify_convert_proof( uint8Ptr(&proof[0]), uint8Ptr(&pubkey[0]), @@ -316,7 +333,7 @@ func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey [PubKeySize]byte, c // VerifyConvertBackProof verifies a linkage + range proof for a ConfidentialMPTConvertBack transaction. // balanceCommit must be the original balance commitment, not the remainder after subtraction, // the C library internally subtracts the transparent amount before checking the range proof. -func VerifyConvertBackProof(proof [ConvertBackProofSize]byte, pubkey [PubKeySize]byte, ciphertext [CiphertextSize]byte, balanceCommit [CommitmentSize]byte, amount uint64, ctxHash [HashOutputSize]byte) error { +func VerifyConvertBackProof(proof [ConvertBackProofSize]byte, pubkey PublicKey, ciphertext Ciphertext, balanceCommit Commitment, amount uint64, ctxHash ContextHash) error { ret := C.mpt_verify_convert_back_proof( uint8Ptr(&proof[0]), uint8Ptr(&pubkey[0]), @@ -332,7 +349,7 @@ func VerifyConvertBackProof(proof [ConvertBackProofSize]byte, pubkey [PubKeySize } // VerifySendProof verifies the full proof for a ConfidentialMPTSend transaction. -func VerifySendProof(proof []byte, participants []Participant, senderCt [CiphertextSize]byte, amountCommit, balanceCommit [CommitmentSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifySendProof(proof []byte, participants []Participant, senderCt Ciphertext, amountCommit, balanceCommit Commitment, ctxHash ContextHash) error { if len(proof) != SendProofSize { return fmt.Errorf("mptcrypto: proof must be %d bytes, got %d", SendProofSize, len(proof)) } @@ -362,7 +379,7 @@ func VerifySendProof(proof []byte, participants []Participant, senderCt [Ciphert } // VerifyClawbackProof verifies an equality proof for a ConfidentialMPTClawback transaction. -func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pubkey [PubKeySize]byte, ciphertext [CiphertextSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pubkey PublicKey, ciphertext Ciphertext, ctxHash ContextHash) error { ret := C.mpt_verify_clawback_proof( uint8Ptr(&proof[0]), C.uint64_t(amount), @@ -382,7 +399,7 @@ func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pu // VerifyRevealedAmount verifies that a revealed amount and blinding factor are consistent // with the participants' ciphertexts. auditor may be nil if no auditor is present. -func VerifyRevealedAmount(amount uint64, bf [BlindingFactorSize]byte, holder, issuer Participant, auditor *Participant) error { +func VerifyRevealedAmount(amount uint64, bf BlindingFactor, holder, issuer Participant, auditor *Participant) error { cHolder := toParticipant(holder) cIssuer := toParticipant(issuer) var cAuditor *C.mpt_confidential_participant @@ -404,7 +421,7 @@ func VerifyRevealedAmount(amount uint64, bf [BlindingFactorSize]byte, holder, is } // VerifySendRangeProof verifies that the transfer amount and remaining balance are within [0, 2^64-1]. -func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment [CommitmentSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) error { ret := C.mpt_verify_send_range_proof( uint8Ptr(&proof[0]), uint8Ptr(&amountCommit[0]), @@ -422,7 +439,7 @@ func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balan // region Utilities // ComputeConvertBackRemainder subtracts a transparent amount from a hidden Pedersen commitment. -func ComputeConvertBackRemainder(commitmentIn [CommitmentSize]byte, amount uint64) (commitmentOut [CommitmentSize]byte, err error) { +func ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (commitmentOut Commitment, err error) { ret := C.mpt_compute_convert_back_remainder( uint8Ptr(&commitmentIn[0]), C.uint64_t(amount), diff --git a/confidential/mptcrypto/mptcrypto_nocgo.go b/confidential/mptcrypto/mptcrypto_nocgo.go index 0018c8146..ea6a1f23c 100644 --- a/confidential/mptcrypto/mptcrypto_nocgo.go +++ b/confidential/mptcrypto/mptcrypto_nocgo.go @@ -2,36 +2,28 @@ package mptcrypto -import "errors" - -// ErrCgoRequired is returned by all crypto functions when built without CGo. -var ErrCgoRequired = errors.New( - "mptcrypto: CGo is required for confidential MPT operations; " + - "rebuild with CGO_ENABLED=1 and vendored mpt-crypto libraries", -) - // region ElGamal // GenerateKeypair creates a new secp256k1 ElGamal keypair. // Returns a 32-byte private key and a 33-byte compressed public key. -func GenerateKeypair() (privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, err error) { +func GenerateKeypair() (privkey PrivateKey, pubkey PublicKey, err error) { return privkey, pubkey, ErrCgoRequired } // GenerateBlindingFactor returns a random 32-byte scalar suitable for ElGamal encryption. -func GenerateBlindingFactor() (bf [BlindingFactorSize]byte, err error) { +func GenerateBlindingFactor() (bf BlindingFactor, err error) { return bf, ErrCgoRequired } // EncryptAmount encrypts a uint64 amount under a compressed public key using a blinding factor. // Returns a 66-byte ciphertext (two compressed EC points: C1 || C2). -func EncryptAmount(amount uint64, pubkey [PubKeySize]byte, bf [BlindingFactorSize]byte) (ct [CiphertextSize]byte, err error) { +func EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (ct Ciphertext, err error) { return ct, ErrCgoRequired } // DecryptAmount decrypts a 66-byte ElGamal ciphertext using a private key. -// Returns the plaintext uint64 amount. -func DecryptAmount(ciphertext [CiphertextSize]byte, privkey [PrivKeySize]byte) (uint64, error) { +// It searches the inclusive [rangeLow, rangeHigh] interval with linear cost. +func DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, rangeHigh uint64) (uint64, error) { return 0, ErrCgoRequired } @@ -40,22 +32,22 @@ func DecryptAmount(ciphertext [CiphertextSize]byte, privkey [PrivKeySize]byte) ( // region Context hashes // ConvertContextHash computes the context hash for a ConfidentialMPTConvert transaction. -func ConvertContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32) (hash [HashOutputSize]byte, err error) { +func ConvertContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32) (hash ContextHash, err error) { return hash, ErrCgoRequired } // ConvertBackContextHash computes the context hash for a ConfidentialMPTConvertBack transaction. -func ConvertBackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq, ver uint32) (hash [HashOutputSize]byte, err error) { +func ConvertBackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq, ver uint32) (hash ContextHash, err error) { return hash, ErrCgoRequired } // SendContextHash computes the context hash for a ConfidentialMPTSend transaction. -func SendContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, dest [AccountIDSize]byte, ver uint32) (hash [HashOutputSize]byte, err error) { +func SendContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, dest [AccountIDSize]byte, ver uint32) (hash ContextHash, err error) { return hash, ErrCgoRequired } // ClawbackContextHash computes the context hash for a ConfidentialMPTClawback transaction. -func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, holder [AccountIDSize]byte) (hash [HashOutputSize]byte, err error) { +func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, seq uint32, holder [AccountIDSize]byte) (hash ContextHash, err error) { return hash, ErrCgoRequired } @@ -64,7 +56,7 @@ func ClawbackContextHash(account [AccountIDSize]byte, iss [IssuanceIDSize]byte, // region Pedersen commitment // PedersenCommitment computes a Pedersen commitment for the given amount and blinding factor. -func PedersenCommitment(amount uint64, bf [BlindingFactorSize]byte) (commitment [CommitmentSize]byte, err error) { +func PedersenCommitment(amount uint64, bf BlindingFactor) (commitment Commitment, err error) { return commitment, ErrCgoRequired } @@ -73,25 +65,25 @@ func PedersenCommitment(amount uint64, bf [BlindingFactorSize]byte) (commitment // region Proof generation // GenerateConvertProof generates a Schnorr proof of knowledge for a ConfidentialMPTConvert transaction. -func GenerateConvertProof(pubkey [PubKeySize]byte, privkey [PrivKeySize]byte, ctxHash [HashOutputSize]byte) (proof [SchnorrProofSize]byte, err error) { +func GenerateConvertProof(pubkey PublicKey, privkey PrivateKey, ctxHash ContextHash) (proof [SchnorrProofSize]byte, err error) { return proof, ErrCgoRequired } // GenerateConvertBackProof generates a compact AND-composed sigma proof over the balance // witness, followed by a single Bulletproof range proof over the remainder commitment, // for a ConfidentialMPTConvertBack transaction. -func GenerateConvertBackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte, amount uint64, params PedersenProofParams) (proof [ConvertBackProofSize]byte, err error) { +func GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) (proof [ConvertBackProofSize]byte, err error) { return proof, ErrCgoRequired } // GenerateClawbackProof generates an equality proof for a ConfidentialMPTClawback transaction. -func GenerateClawbackProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte, amount uint64, ciphertext [CiphertextSize]byte) (proof [CompactClawbackProofSize]byte, err error) { +func GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) (proof [CompactClawbackProofSize]byte, err error) { return proof, ErrCgoRequired } // GenerateSendProof generates a compact AND-composed sigma proof + aggregated Bulletproof range proof // for a ConfidentialMPTSend transaction. -func GenerateSendProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, amount uint64, participants []Participant, txBF [BlindingFactorSize]byte, ctxHash [HashOutputSize]byte, amountCommitment [CommitmentSize]byte, balanceParams PedersenProofParams) ([]byte, error) { +func GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error) { return nil, ErrCgoRequired } @@ -100,24 +92,24 @@ func GenerateSendProof(privkey [PrivKeySize]byte, pubkey [PubKeySize]byte, amoun // region Proof verification (top-level) // VerifyConvertProof verifies a Schnorr proof for a ConfidentialMPTConvert transaction. -func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey [PubKeySize]byte, ctxHash [HashOutputSize]byte) error { +func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey PublicKey, ctxHash ContextHash) error { return ErrCgoRequired } // VerifyConvertBackProof verifies a linkage + range proof for a ConfidentialMPTConvertBack transaction. -// balanceCommit must be the original balance commitment, not the remainder after subtraction; +// balanceCommit must be the original balance commitment, not the remainder after subtraction, // the C library internally subtracts the transparent amount before checking the range proof. -func VerifyConvertBackProof(proof [ConvertBackProofSize]byte, pubkey [PubKeySize]byte, ciphertext [CiphertextSize]byte, balanceCommit [CommitmentSize]byte, amount uint64, ctxHash [HashOutputSize]byte) error { +func VerifyConvertBackProof(proof [ConvertBackProofSize]byte, pubkey PublicKey, ciphertext Ciphertext, balanceCommit Commitment, amount uint64, ctxHash ContextHash) error { return ErrCgoRequired } // VerifySendProof verifies the full proof for a ConfidentialMPTSend transaction. -func VerifySendProof(proof []byte, participants []Participant, senderCt [CiphertextSize]byte, amountCommit, balanceCommit [CommitmentSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifySendProof(proof []byte, participants []Participant, senderCt Ciphertext, amountCommit, balanceCommit Commitment, ctxHash ContextHash) error { return ErrCgoRequired } // VerifyClawbackProof verifies an equality proof for a ConfidentialMPTClawback transaction. -func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pubkey [PubKeySize]byte, ciphertext [CiphertextSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pubkey PublicKey, ciphertext Ciphertext, ctxHash ContextHash) error { return ErrCgoRequired } @@ -126,13 +118,13 @@ func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pu // region Internal component verifiers // VerifyRevealedAmount verifies that a revealed amount and blinding factor are consistent -// with the participants' ciphertexts. -func VerifyRevealedAmount(amount uint64, bf [BlindingFactorSize]byte, holder, issuer Participant, auditor *Participant) error { +// with the participants' ciphertexts. auditor may be nil if no auditor is present. +func VerifyRevealedAmount(amount uint64, bf BlindingFactor, holder, issuer Participant, auditor *Participant) error { return ErrCgoRequired } // VerifySendRangeProof verifies that the transfer amount and remaining balance are within [0, 2^64-1]. -func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment [CommitmentSize]byte, ctxHash [HashOutputSize]byte) error { +func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) error { return ErrCgoRequired } @@ -141,7 +133,7 @@ func VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balan // region Utilities // ComputeConvertBackRemainder subtracts a transparent amount from a hidden Pedersen commitment. -func ComputeConvertBackRemainder(commitmentIn [CommitmentSize]byte, amount uint64) (commitmentOut [CommitmentSize]byte, err error) { +func ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (commitmentOut Commitment, err error) { return commitmentOut, ErrCgoRequired } diff --git a/confidential/mptcrypto/mptcrypto_nocgo_test.go b/confidential/mptcrypto/mptcrypto_nocgo_test.go new file mode 100644 index 000000000..a25ec341d --- /dev/null +++ b/confidential/mptcrypto/mptcrypto_nocgo_test.go @@ -0,0 +1,15 @@ +//go:build !cgo || js || wasip1 || tinygo || gofuzz || !(linux || darwin) || !(amd64 || arm64) + +package mptcrypto_test + +import ( + "testing" + + "github.com/Peersyst/xrpl-go/confidential/mptcrypto" + "github.com/stretchr/testify/require" +) + +func TestDecryptAmountWithoutCgo(t *testing.T) { + _, err := mptcrypto.DecryptAmount(mptcrypto.Ciphertext{}, mptcrypto.PrivateKey{}, 2, 1) + require.ErrorIs(t, err, mptcrypto.ErrCgoRequired) +} diff --git a/confidential/mptcrypto/mptcrypto_test.go b/confidential/mptcrypto/mptcrypto_test.go index df7b1c3ec..f06876666 100644 --- a/confidential/mptcrypto/mptcrypto_test.go +++ b/confidential/mptcrypto/mptcrypto_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package mptcrypto_test @@ -33,7 +33,7 @@ func testIssuanceID() [mptcrypto.IssuanceIDSize]byte { func TestGenerateKeypair(t *testing.T) { priv, pub, err := mptcrypto.GenerateKeypair() require.NoError(t, err) - require.NotEqual(t, [mptcrypto.PrivKeySize]byte{}, priv, "privkey is all zeros") + require.NotEqual(t, mptcrypto.PrivateKey{}, priv, "privkey is all zeros") // compressed secp256k1 pubkey starts with 0x02 or 0x03 require.Contains(t, []byte{0x02, 0x03}, pub[0], "unexpected pubkey prefix: 0x%02x", pub[0]) } @@ -41,7 +41,7 @@ func TestGenerateKeypair(t *testing.T) { func TestGenerateBlindingFactor(t *testing.T) { bf1, err := mptcrypto.GenerateBlindingFactor() require.NoError(t, err) - require.NotEqual(t, [mptcrypto.BlindingFactorSize]byte{}, bf1, "blinding factor is all zeros") + require.NotEqual(t, mptcrypto.BlindingFactor{}, bf1, "blinding factor is all zeros") // two calls should produce different values (non-deterministic RNG) bf2, err := mptcrypto.GenerateBlindingFactor() @@ -49,38 +49,58 @@ func TestGenerateBlindingFactor(t *testing.T) { require.NotEqual(t, bf1, bf2, "two consecutive blinding factors are identical") } -func TestEncryptDecryptRoundtrip(t *testing.T) { +func TestDecryptAmountBounds(t *testing.T) { + // amount is an arbitrary non-boundary value used to exercise the range checks. + const amount uint64 = 42 + + privateKey, publicKey, err := mptcrypto.GenerateKeypair() + require.NoError(t, err) + blindingFactor, err := mptcrypto.GenerateBlindingFactor() + require.NoError(t, err) + ciphertext, err := mptcrypto.EncryptAmount(amount, publicKey, blindingFactor) + require.NoError(t, err) + tests := []struct { - name string - amount uint64 - // skipOnDecryptErr skips the subtest instead of failing when the C - // library cannot recover the plaintext (BSGS table limitation). - skipOnDecryptErr bool + name string + rangeLow uint64 + rangeHigh uint64 + wantErr bool }{ - {"pass - zero", 0, false}, - {"pass - small value", 42, false}, - {"pass - one million", 1_000_000, false}, - {"pass - max uint64", math.MaxUint64, true}, + {name: "pass - found at lower bound", rangeLow: amount, rangeHigh: 50}, + {name: "pass - found at upper bound", rangeLow: 40, rangeHigh: amount}, + {name: "pass - found inside range", rangeLow: 40, rangeHigh: 50}, + {name: "pass - single-value interval", rangeLow: amount, rangeHigh: amount}, + {name: "fail - amount below range", rangeLow: 43, rangeHigh: 50, wantErr: true}, + {name: "fail - amount above range", rangeLow: 0, rangeHigh: 41, wantErr: true}, } - priv, pub, err := mptcrypto.GenerateKeypair() - require.NoError(t, err) - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - bf, err := mptcrypto.GenerateBlindingFactor() + got, err := mptcrypto.DecryptAmount(ciphertext, privateKey, tt.rangeLow, tt.rangeHigh) + if tt.wantErr { + require.Error(t, err) + return + } require.NoError(t, err) + require.Equal(t, amount, got) + }) + } +} - ct, err := mptcrypto.EncryptAmount(tt.amount, pub, bf) - require.NoError(t, err) - require.NotEqual(t, [mptcrypto.CiphertextSize]byte{}, ct, "ciphertext is all zeros") +func TestDecryptAmountInvalidRange(t *testing.T) { + tests := []struct { + name string + rangeLow uint64 + rangeHigh uint64 + }{ + {name: "fail - low exceeds high", rangeLow: 2, rangeHigh: 1}, + {name: "fail - high is max uint64", rangeLow: 0, rangeHigh: math.MaxUint64}, + } - got, err := mptcrypto.DecryptAmount(ct, priv) - if err != nil && tt.skipOnDecryptErr { - t.Skipf("DecryptAmount not supported for this value: %v", err) - } - require.NoError(t, err) - require.Equal(t, tt.amount, got) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := mptcrypto.DecryptAmount(mptcrypto.Ciphertext{}, mptcrypto.PrivateKey{}, tt.rangeLow, tt.rangeHigh) + require.ErrorIs(t, err, mptcrypto.ErrInvalidAmountRange) }) } } @@ -88,7 +108,7 @@ func TestEncryptDecryptRoundtrip(t *testing.T) { // endregion // region Context hashes -type contextHashFn func() ([mptcrypto.HashOutputSize]byte, error) +type contextHashFn func() (mptcrypto.ContextHash, error) func TestContextHashes(t *testing.T) { account := testAccountID(0x01) @@ -102,33 +122,33 @@ func TestContextHashes(t *testing.T) { }{ { "pass - Convert", - func() ([mptcrypto.HashOutputSize]byte, error) { return mptcrypto.ConvertContextHash(account, iss, 1) }, - func() ([mptcrypto.HashOutputSize]byte, error) { return mptcrypto.ConvertContextHash(account, iss, 2) }, + func() (mptcrypto.ContextHash, error) { return mptcrypto.ConvertContextHash(account, iss, 1) }, + func() (mptcrypto.ContextHash, error) { return mptcrypto.ConvertContextHash(account, iss, 2) }, }, { "pass - ConvertBack", - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.ConvertBackContextHash(account, iss, 1, 1) }, - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.ConvertBackContextHash(account, iss, 1, 2) }, }, { "pass - Send", - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.SendContextHash(account, iss, 1, account2, 1) }, - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.SendContextHash(account, iss, 1, testAccountID(0x30), 1) }, }, { "pass - Clawback", - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.ClawbackContextHash(account, iss, 1, account2) }, - func() ([mptcrypto.HashOutputSize]byte, error) { + func() (mptcrypto.ContextHash, error) { return mptcrypto.ClawbackContextHash(account, iss, 1, testAccountID(0x30)) }, }, @@ -138,7 +158,7 @@ func TestContextHashes(t *testing.T) { t.Run(tt.name, func(t *testing.T) { hash, err := tt.hash() require.NoError(t, err) - require.NotEqual(t, [mptcrypto.HashOutputSize]byte{}, hash) + require.NotEqual(t, mptcrypto.ContextHash{}, hash) // deterministic hash2, err := tt.hash() @@ -160,23 +180,31 @@ func TestPedersenCommitment(t *testing.T) { bf, err := mptcrypto.GenerateBlindingFactor() require.NoError(t, err) - t.Run("pass - valid prefix and deterministic", func(t *testing.T) { - commit, err := mptcrypto.PedersenCommitment(42, bf) - require.NoError(t, err) - require.Contains(t, []byte{0x02, 0x03}, commit[0], "unexpected commitment prefix: 0x%02x", commit[0]) + tests := []struct { + name string + first uint64 + second uint64 + wantEqual bool + }{ + {name: "pass - same inputs are deterministic", first: 42, second: 42, wantEqual: true}, + {name: "pass - different amounts", first: 42, second: 99}, + } - commit2, err := mptcrypto.PedersenCommitment(42, bf) - require.NoError(t, err) - require.Equal(t, commit, commit2) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + first, err := mptcrypto.PedersenCommitment(tt.first, bf) + require.NoError(t, err) + require.Contains(t, []byte{0x02, 0x03}, first[0], "unexpected commitment prefix: 0x%02x", first[0]) - t.Run("pass - different amounts produce different commitments", func(t *testing.T) { - commit, err := mptcrypto.PedersenCommitment(42, bf) - require.NoError(t, err) - commit2, err := mptcrypto.PedersenCommitment(99, bf) - require.NoError(t, err) - require.NotEqual(t, commit, commit2) - }) + second, err := mptcrypto.PedersenCommitment(tt.second, bf) + require.NoError(t, err) + if tt.wantEqual { + require.Equal(t, first, second) + return + } + require.NotEqual(t, first, second) + }) + } } // endregion @@ -185,25 +213,35 @@ func TestPedersenCommitment(t *testing.T) { func TestConvertProofRoundtrip(t *testing.T) { priv, pub, err := mptcrypto.GenerateKeypair() require.NoError(t, err) + _, wrongPub, err := mptcrypto.GenerateKeypair() + require.NoError(t, err) ctxHash, err := mptcrypto.ConvertContextHash(testAccountID(0x01), testIssuanceID(), 1) require.NoError(t, err) proof, err := mptcrypto.GenerateConvertProof(pub, priv, ctxHash) require.NoError(t, err) + require.NotEqual(t, [mptcrypto.SchnorrProofSize]byte{}, proof) - t.Run("pass - valid proof verifies", func(t *testing.T) { - require.NotEqual(t, [mptcrypto.SchnorrProofSize]byte{}, proof) - err := mptcrypto.VerifyConvertProof(proof, pub, ctxHash) - require.NoError(t, err) - }) + tests := []struct { + name string + pubKey mptcrypto.PublicKey + wantErr bool + }{ + {name: "pass - valid proof verifies", pubKey: pub}, + {name: "fail - wrong key rejected", pubKey: wrongPub, wantErr: true}, + } - t.Run("fail - wrong key rejected", func(t *testing.T) { - _, wrongPub, err := mptcrypto.GenerateKeypair() - require.NoError(t, err) - err = mptcrypto.VerifyConvertProof(proof, wrongPub, ctxHash) - require.Error(t, err) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mptcrypto.VerifyConvertProof(proof, tt.pubKey, ctxHash) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } } func TestConvertBackProofRoundtrip(t *testing.T) { @@ -347,9 +385,9 @@ func TestSendProofRoundtrip(t *testing.T) { func TestVerifySendProofRejectsShortProof(t *testing.T) { shortProof := make([]byte, mptcrypto.SendProofSize-1) - var senderCT [mptcrypto.CiphertextSize]byte - var amountCommit, balanceCommit [mptcrypto.CommitmentSize]byte - var ctxHash [mptcrypto.HashOutputSize]byte + var senderCT mptcrypto.Ciphertext + var amountCommit, balanceCommit mptcrypto.Commitment + var ctxHash mptcrypto.ContextHash err := mptcrypto.VerifySendProof(shortProof, nil, senderCT, amountCommit, balanceCommit, ctxHash) require.EqualError(t, err, fmt.Sprintf("mptcrypto: proof must be %d bytes, got %d", mptcrypto.SendProofSize, len(shortProof))) @@ -359,7 +397,8 @@ func TestVerifySendProofRejectsShortProof(t *testing.T) { // region Internal component verifiers func TestVerifyRevealedAmount(t *testing.T) { - amount := uint64(42) + const amount uint64 = 42 + bf, err := mptcrypto.GenerateBlindingFactor() require.NoError(t, err) @@ -367,35 +406,41 @@ func TestVerifyRevealedAmount(t *testing.T) { require.NoError(t, err) _, issuerPub, err := mptcrypto.GenerateKeypair() require.NoError(t, err) + _, auditorPub, err := mptcrypto.GenerateKeypair() + require.NoError(t, err) holderCT, err := mptcrypto.EncryptAmount(amount, holderPub, bf) require.NoError(t, err) issuerCT, err := mptcrypto.EncryptAmount(amount, issuerPub, bf) require.NoError(t, err) + auditorCT, err := mptcrypto.EncryptAmount(amount, auditorPub, bf) + require.NoError(t, err) holder := mptcrypto.Participant{PubKey: holderPub, Ciphertext: holderCT} issuer := mptcrypto.Participant{PubKey: issuerPub, Ciphertext: issuerCT} + auditor := &mptcrypto.Participant{PubKey: auditorPub, Ciphertext: auditorCT} + + tests := []struct { + name string + verifyAmount uint64 + auditor *mptcrypto.Participant + wantErr bool + }{ + {name: "pass - without auditor", verifyAmount: amount}, + {name: "pass - with auditor", verifyAmount: amount, auditor: auditor}, + {name: "fail - wrong amount", verifyAmount: 99, wantErr: true}, + } - t.Run("pass - without auditor", func(t *testing.T) { - err := mptcrypto.VerifyRevealedAmount(amount, bf, holder, issuer, nil) - require.NoError(t, err) - }) - - t.Run("pass - with auditor", func(t *testing.T) { - _, auditorPub, err := mptcrypto.GenerateKeypair() - require.NoError(t, err) - auditorCT, err := mptcrypto.EncryptAmount(amount, auditorPub, bf) - require.NoError(t, err) - auditor := &mptcrypto.Participant{PubKey: auditorPub, Ciphertext: auditorCT} - - err = mptcrypto.VerifyRevealedAmount(amount, bf, holder, issuer, auditor) - require.NoError(t, err) - }) - - t.Run("fail - wrong amount", func(t *testing.T) { - err := mptcrypto.VerifyRevealedAmount(99, bf, holder, issuer, nil) - require.Error(t, err) - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mptcrypto.VerifyRevealedAmount(tt.verifyAmount, bf, holder, issuer, tt.auditor) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } } // endregion diff --git a/confidential/mptcrypto/types.go b/confidential/mptcrypto/types.go index 39bb47348..659bc844e 100644 --- a/confidential/mptcrypto/types.go +++ b/confidential/mptcrypto/types.go @@ -29,16 +29,34 @@ const ( MaxParticipants = math.MaxUint8 // C API uses uint8_t for participant count ) +// PrivateKey is a fixed-size ElGamal private key. +type PrivateKey [PrivKeySize]byte + +// PublicKey is a fixed-size compressed ElGamal public key. +type PublicKey [PubKeySize]byte + +// BlindingFactor is a fixed-size scalar used for encryption and commitments. +type BlindingFactor [BlindingFactorSize]byte + +// Ciphertext is a fixed-size ElGamal ciphertext. +type Ciphertext [CiphertextSize]byte + +// Commitment is a fixed-size compressed Pedersen commitment. +type Commitment [CommitmentSize]byte + +// ContextHash binds a proof to its transaction context. +type ContextHash [HashOutputSize]byte + // Participant represents a party in a Confidential Send transaction. type Participant struct { - PubKey [PubKeySize]byte - Ciphertext [CiphertextSize]byte + PubKey PublicKey + Ciphertext Ciphertext } // PedersenProofParams holds the parameters required to generate a Pedersen linkage proof. type PedersenProofParams struct { - Commitment [CommitmentSize]byte + Commitment Commitment Amount uint64 - Ciphertext [CiphertextSize]byte - BlindingFactor [BlindingFactorSize]byte + Ciphertext Ciphertext + BlindingFactor BlindingFactor } diff --git a/confidential/proof/clawback.go b/confidential/proof/clawback.go index 9f0d491f7..080ff1c41 100644 --- a/confidential/proof/clawback.go +++ b/confidential/proof/clawback.go @@ -28,14 +28,10 @@ func GenerateClawbackProof(privkeyHex, pubkeyHex, ctxHashHex string, amount uint return "", fmt.Errorf("%w: %w", ErrInvalidCiphertext, err) } - var priv [mptcrypto.PrivKeySize]byte - var pub [mptcrypto.PubKeySize]byte - var hash [mptcrypto.HashOutputSize]byte - var ct [mptcrypto.CiphertextSize]byte - copy(priv[:], privBytes) - copy(pub[:], pubBytes) - copy(hash[:], hashBytes) - copy(ct[:], ctBytes) + priv := mptcrypto.PrivateKey(privBytes) + pub := mptcrypto.PublicKey(pubBytes) + hash := mptcrypto.ContextHash(hashBytes) + ct := mptcrypto.Ciphertext(ctBytes) proof, err := mptcrypto.GenerateClawbackProof(priv, pub, hash, amount, ct) if err != nil { @@ -64,13 +60,10 @@ func VerifyClawbackProof(proofHex string, amount uint64, pubkeyHex, ciphertextHe } var proof [mptcrypto.CompactClawbackProofSize]byte - var pub [mptcrypto.PubKeySize]byte - var ct [mptcrypto.CiphertextSize]byte - var hash [mptcrypto.HashOutputSize]byte copy(proof[:], proofBytes) - copy(pub[:], pubBytes) - copy(ct[:], ctBytes) - copy(hash[:], hashBytes) + pub := mptcrypto.PublicKey(pubBytes) + ct := mptcrypto.Ciphertext(ctBytes) + hash := mptcrypto.ContextHash(hashBytes) if err := mptcrypto.VerifyClawbackProof(proof, amount, pub, ct, hash); err != nil { return fmt.Errorf("%w: %w", ErrProofVerificationFailed, err) diff --git a/confidential/proof/clawback_test.go b/confidential/proof/clawback_test.go index 862fa406a..d6427665a 100644 --- a/confidential/proof/clawback_test.go +++ b/confidential/proof/clawback_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test diff --git a/confidential/proof/context_test.go b/confidential/proof/context_test.go index a15186071..0f0449c6f 100644 --- a/confidential/proof/context_test.go +++ b/confidential/proof/context_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test diff --git a/confidential/proof/convert.go b/confidential/proof/convert.go index 005301020..be8f98c5f 100644 --- a/confidential/proof/convert.go +++ b/confidential/proof/convert.go @@ -25,12 +25,9 @@ func GenerateConvertProof(pubkeyHex, privkeyHex, ctxHashHex string) (string, err return "", fmt.Errorf("%w: %w", ErrInvalidContextHash, err) } - var pub [mptcrypto.PubKeySize]byte - var priv [mptcrypto.PrivKeySize]byte - var hash [mptcrypto.HashOutputSize]byte - copy(pub[:], pubBytes) - copy(priv[:], privBytes) - copy(hash[:], hashBytes) + pub := mptcrypto.PublicKey(pubBytes) + priv := mptcrypto.PrivateKey(privBytes) + hash := mptcrypto.ContextHash(hashBytes) proof, err := mptcrypto.GenerateConvertProof(pub, priv, hash) if err != nil { @@ -56,11 +53,9 @@ func VerifyConvertProof(proofHex, pubkeyHex, ctxHashHex string) error { } var proof [mptcrypto.SchnorrProofSize]byte - var pub [mptcrypto.PubKeySize]byte - var hash [mptcrypto.HashOutputSize]byte copy(proof[:], proofBytes) - copy(pub[:], pubBytes) - copy(hash[:], hashBytes) + pub := mptcrypto.PublicKey(pubBytes) + hash := mptcrypto.ContextHash(hashBytes) if err := mptcrypto.VerifyConvertProof(proof, pub, hash); err != nil { return fmt.Errorf("%w: %w", ErrProofVerificationFailed, err) diff --git a/confidential/proof/convert_back.go b/confidential/proof/convert_back.go index a98eda58a..8c5a6c2ba 100644 --- a/confidential/proof/convert_back.go +++ b/confidential/proof/convert_back.go @@ -28,12 +28,9 @@ func GenerateConvertBackProof(privkeyHex, pubkeyHex, ctxHashHex string, amount u return "", err } - var priv [mptcrypto.PrivKeySize]byte - var pub [mptcrypto.PubKeySize]byte - var hash [mptcrypto.HashOutputSize]byte - copy(priv[:], privBytes) - copy(pub[:], pubBytes) - copy(hash[:], hashBytes) + priv := mptcrypto.PrivateKey(privBytes) + pub := mptcrypto.PublicKey(pubBytes) + hash := mptcrypto.ContextHash(hashBytes) proof, err := mptcrypto.GenerateConvertBackProof(priv, pub, hash, amount, pp) if err != nil { @@ -66,15 +63,11 @@ func VerifyConvertBackProof(proofHex, pubkeyHex, ciphertextHex, balanceCommitHex } var proof [mptcrypto.ConvertBackProofSize]byte - var pub [mptcrypto.PubKeySize]byte - var ct [mptcrypto.CiphertextSize]byte - var commit [mptcrypto.CommitmentSize]byte - var hash [mptcrypto.HashOutputSize]byte copy(proof[:], proofBytes) - copy(pub[:], pubBytes) - copy(ct[:], ctBytes) - copy(commit[:], commitBytes) - copy(hash[:], hashBytes) + pub := mptcrypto.PublicKey(pubBytes) + ct := mptcrypto.Ciphertext(ctBytes) + commit := mptcrypto.Commitment(commitBytes) + hash := mptcrypto.ContextHash(hashBytes) if err := mptcrypto.VerifyConvertBackProof(proof, pub, ct, commit, amount, hash); err != nil { return fmt.Errorf("%w: %w", ErrProofVerificationFailed, err) diff --git a/confidential/proof/convert_back_test.go b/confidential/proof/convert_back_test.go index 4e44177a7..0b2e41aec 100644 --- a/confidential/proof/convert_back_test.go +++ b/confidential/proof/convert_back_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test diff --git a/confidential/proof/convert_test.go b/confidential/proof/convert_test.go index 6f9e9eb44..2a992828e 100644 --- a/confidential/proof/convert_test.go +++ b/confidential/proof/convert_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test @@ -14,6 +14,8 @@ import ( func TestGenerateAndVerifyConvertProof(t *testing.T) { kp, err := elgamal.GenerateKeypair() require.NoError(t, err) + wrongKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) ctxHash, err := proof.ConvertContextHash(testAccount, testIssuanceID, 1) require.NoError(t, err) @@ -22,24 +24,21 @@ func TestGenerateAndVerifyConvertProof(t *testing.T) { require.NoError(t, err) require.Len(t, proofHex, mptcrypto.SchnorrProofSize*2) - err = proof.VerifyConvertProof(proofHex, kp.PubKeyHex, ctxHash) - require.NoError(t, err) -} - -func TestVerifyConvertProofWrongKey(t *testing.T) { - kp1, err := elgamal.GenerateKeypair() - require.NoError(t, err) - kp2, err := elgamal.GenerateKeypair() - require.NoError(t, err) - - ctxHash, err := proof.ConvertContextHash(testAccount, testIssuanceID, 1) - require.NoError(t, err) - - proofHex, err := proof.GenerateConvertProof(kp1.PubKeyHex, kp1.PrivKeyHex, ctxHash) - require.NoError(t, err) + tests := []struct { + name string + pubKey string + wantErr error + }{ + {name: "pass - correct key", pubKey: kp.PubKeyHex}, + {name: "fail - wrong key", pubKey: wrongKP.PubKeyHex, wantErr: proof.ErrProofVerificationFailed}, + } - err = proof.VerifyConvertProof(proofHex, kp2.PubKeyHex, ctxHash) - require.ErrorIs(t, err, proof.ErrProofVerificationFailed) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := proof.VerifyConvertProof(proofHex, tt.pubKey, ctxHash) + require.ErrorIs(t, err, tt.wantErr) + }) + } } func TestConvertProofInvalidInputs(t *testing.T) { diff --git a/confidential/proof/helpers.go b/confidential/proof/helpers.go index 04dca95fa..52649a773 100644 --- a/confidential/proof/helpers.go +++ b/confidential/proof/helpers.go @@ -41,8 +41,8 @@ func decodeParticipant(hp Participant) (mptcrypto.Participant, error) { if err != nil { return p, fmt.Errorf("%w: %w", ErrInvalidCiphertext, err) } - copy(p.PubKey[:], pubBytes) - copy(p.Ciphertext[:], ctBytes) + p.PubKey = mptcrypto.PublicKey(pubBytes) + p.Ciphertext = mptcrypto.Ciphertext(ctBytes) return p, nil } @@ -77,9 +77,9 @@ func decodeProofParams(hp Params) (mptcrypto.PedersenProofParams, error) { if err != nil { return p, fmt.Errorf("%w: %w", ErrInvalidBlindingFactor, err) } - copy(p.Commitment[:], commitBytes) + p.Commitment = mptcrypto.Commitment(commitBytes) p.Amount = hp.Amount - copy(p.Ciphertext[:], ctBytes) - copy(p.BlindingFactor[:], bfBytes) + p.Ciphertext = mptcrypto.Ciphertext(ctBytes) + p.BlindingFactor = mptcrypto.BlindingFactor(bfBytes) return p, nil } diff --git a/confidential/proof/helpers_test.go b/confidential/proof/helpers_test.go index d418bcca9..b6e45cb21 100644 --- a/confidential/proof/helpers_test.go +++ b/confidential/proof/helpers_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test diff --git a/confidential/proof/send.go b/confidential/proof/send.go index 3a0c82e07..ebfa2758b 100644 --- a/confidential/proof/send.go +++ b/confidential/proof/send.go @@ -41,14 +41,10 @@ func GenerateSendProof(privkeyHex string, pubkeyHex string, amount uint64, parti return "", err } - var priv [mptcrypto.PrivKeySize]byte - var pub [mptcrypto.PubKeySize]byte - var bf [mptcrypto.BlindingFactorSize]byte - var hash [mptcrypto.HashOutputSize]byte - copy(priv[:], privBytes) - copy(pub[:], pubBytes) - copy(bf[:], bfBytes) - copy(hash[:], hashBytes) + priv := mptcrypto.PrivateKey(privBytes) + pub := mptcrypto.PublicKey(pubBytes) + bf := mptcrypto.BlindingFactor(bfBytes) + hash := mptcrypto.ContextHash(hashBytes) proof, err := mptcrypto.GenerateSendProof(priv, pub, amount, parts, bf, hash, ap.Commitment, bp) if err != nil { @@ -85,14 +81,10 @@ func VerifySendProof(proofHex string, participants []Participant, senderCtHex, a return fmt.Errorf("%w: %w", ErrInvalidContextHash, err) } - var senderCt [mptcrypto.CiphertextSize]byte - var amountCommit [mptcrypto.CommitmentSize]byte - var balanceCommit [mptcrypto.CommitmentSize]byte - var hash [mptcrypto.HashOutputSize]byte - copy(senderCt[:], senderCtBytes) - copy(amountCommit[:], amountCommitBytes) - copy(balanceCommit[:], balanceCommitBytes) - copy(hash[:], hashBytes) + senderCt := mptcrypto.Ciphertext(senderCtBytes) + amountCommit := mptcrypto.Commitment(amountCommitBytes) + balanceCommit := mptcrypto.Commitment(balanceCommitBytes) + hash := mptcrypto.ContextHash(hashBytes) if err := mptcrypto.VerifySendProof(proofBytes, parts, senderCt, amountCommit, balanceCommit, hash); err != nil { return fmt.Errorf("%w: %w", ErrProofVerificationFailed, err) diff --git a/confidential/proof/send_test.go b/confidential/proof/send_test.go index 5d3d17a64..7413be356 100644 --- a/confidential/proof/send_test.go +++ b/confidential/proof/send_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test diff --git a/confidential/proof/verify.go b/confidential/proof/verify.go index 06a5e0f2a..a55ae6097 100644 --- a/confidential/proof/verify.go +++ b/confidential/proof/verify.go @@ -23,8 +23,7 @@ func VerifyRevealedAmount(amount uint64, bfHex string, holder, issuer Participan return err } - var bf [mptcrypto.BlindingFactorSize]byte - copy(bf[:], bfBytes) + bf := mptcrypto.BlindingFactor(bfBytes) var auditorP *mptcrypto.Participant if auditor != nil { @@ -62,13 +61,10 @@ func VerifySendRangeProof(proofHex, amountCommitHex, balanceCommitHex, ctxHashHe } var proof [mptcrypto.DoubleBulletproofSize]byte - var amountCommit [mptcrypto.CommitmentSize]byte - var balanceCommit [mptcrypto.CommitmentSize]byte - var hash [mptcrypto.HashOutputSize]byte copy(proof[:], proofBytes) - copy(amountCommit[:], amountCommitBytes) - copy(balanceCommit[:], balanceCommitBytes) - copy(hash[:], hashBytes) + amountCommit := mptcrypto.Commitment(amountCommitBytes) + balanceCommit := mptcrypto.Commitment(balanceCommitBytes) + hash := mptcrypto.ContextHash(hashBytes) if err := mptcrypto.VerifySendRangeProof(proof, amountCommit, balanceCommit, hash); err != nil { return fmt.Errorf("%w: %w", ErrProofVerificationFailed, err) diff --git a/confidential/proof/verify_test.go b/confidential/proof/verify_test.go index 37a118391..8707987af 100644 --- a/confidential/proof/verify_test.go +++ b/confidential/proof/verify_test.go @@ -1,4 +1,4 @@ -//go:build cgo +//go:build cgo && !js && !wasip1 && !tinygo && !gofuzz && (linux || darwin) && (amd64 || arm64) package proof_test @@ -11,30 +11,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestVerifyRevealedAmountWithoutAuditor(t *testing.T) { - const amount uint64 = 42 - - holderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - - bf, err := elgamal.GenerateBlindingFactor() - require.NoError(t, err) - - holderCt, err := elgamal.Encrypt(amount, holderKP.PubKeyHex, bf) - require.NoError(t, err) - issuerCt, err := elgamal.Encrypt(amount, issuerKP.PubKeyHex, bf) - require.NoError(t, err) - - holder := proof.Participant{PubKeyHex: holderKP.PubKeyHex, CiphertextHex: holderCt} - issuer := proof.Participant{PubKeyHex: issuerKP.PubKeyHex, CiphertextHex: issuerCt} - - err = proof.VerifyRevealedAmount(amount, bf, holder, issuer, nil) - require.NoError(t, err) -} - -func TestVerifyRevealedAmountWithAuditor(t *testing.T) { +func TestVerifyRevealedAmount(t *testing.T) { const amount uint64 = 42 holderKP, err := elgamal.GenerateKeypair() @@ -56,33 +33,25 @@ func TestVerifyRevealedAmountWithAuditor(t *testing.T) { holder := proof.Participant{PubKeyHex: holderKP.PubKeyHex, CiphertextHex: holderCt} issuer := proof.Participant{PubKeyHex: issuerKP.PubKeyHex, CiphertextHex: issuerCt} - auditor := proof.Participant{PubKeyHex: auditorKP.PubKeyHex, CiphertextHex: auditorCt} - - err = proof.VerifyRevealedAmount(amount, bf, holder, issuer, &auditor) - require.NoError(t, err) -} + auditor := &proof.Participant{PubKeyHex: auditorKP.PubKeyHex, CiphertextHex: auditorCt} -func TestVerifyRevealedAmountWrongAmount(t *testing.T) { - const amount uint64 = 42 - - holderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - - bf, err := elgamal.GenerateBlindingFactor() - require.NoError(t, err) - - holderCt, err := elgamal.Encrypt(amount, holderKP.PubKeyHex, bf) - require.NoError(t, err) - issuerCt, err := elgamal.Encrypt(amount, issuerKP.PubKeyHex, bf) - require.NoError(t, err) - - holder := proof.Participant{PubKeyHex: holderKP.PubKeyHex, CiphertextHex: holderCt} - issuer := proof.Participant{PubKeyHex: issuerKP.PubKeyHex, CiphertextHex: issuerCt} + tests := []struct { + name string + verifyAmount uint64 + auditor *proof.Participant + wantErr error + }{ + {name: "pass - without auditor", verifyAmount: amount}, + {name: "pass - with auditor", verifyAmount: amount, auditor: auditor}, + {name: "fail - wrong amount", verifyAmount: 999, wantErr: proof.ErrProofVerificationFailed}, + } - err = proof.VerifyRevealedAmount(999, bf, holder, issuer, nil) - require.ErrorIs(t, err, proof.ErrProofVerificationFailed) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := proof.VerifyRevealedAmount(tt.verifyAmount, bf, holder, issuer, tt.auditor) + require.ErrorIs(t, err, tt.wantErr) + }) + } } func TestVerifyRevealedAmountInvalidInputs(t *testing.T) { diff --git a/docs/docs/confidential/builders.md b/docs/docs/confidential/builders.md index 3ea40bb37..282cf9a6b 100644 --- a/docs/docs/confidential/builders.md +++ b/docs/docs/confidential/builders.md @@ -52,7 +52,7 @@ Use these for `ConfidentialMPTSend`. - Resolves issuer, auditor, sender, and destination encryption keys. - Reads the sender `MPToken` state, including `ConfidentialBalanceSpending` and `ConfidentialBalanceVersion`. -- Decrypts the sender's current confidential balance with the supplied private key. +- Decrypts the sender's current confidential balance with the supplied private key and inclusive `BalanceRange`. - Encrypts the transfer amount for sender, destination, issuer, and optional auditor. - Builds both Pedersen commitments and the composite send proof. @@ -66,6 +66,10 @@ tx, err := builder.BuildSend(client, builder.BuildSendParams{ Amount: 25, SenderPrivKey: senderPrivKeyHex, SenderPubKey: senderPubKeyHex, + BalanceRange: elgamal.AmountRange{ + Low: 0, + High: 1_000_000, + }, }) ``` @@ -74,7 +78,7 @@ tx, err := builder.BuildSend(client, builder.BuildSendParams{ Use these for `ConfidentialMPTConvertBack`. - Resolves issuer and optional auditor keys. -- Reads and decrypts the holder's current confidential spending balance. +- Reads and decrypts the holder's current confidential spending balance within the supplied inclusive `BalanceRange`. - Uses `ConfidentialBalanceVersion` from ledger state. - Builds the encrypted withdrawal amount, balance commitment, and convert-back proof. @@ -85,9 +89,21 @@ tx, err := builder.BuildConvertBack(client, builder.BuildConvertBackParams{ Amount: 10, HolderPrivKey: holderPrivKeyHex, HolderPubKey: holderPubKeyHex, + BalanceRange: elgamal.AmountRange{ + Low: 0, + High: 1_000_000, + }, }) ``` +### Bounded balance decryption + +`BuildSend` and `BuildConvertBack` decrypt the current on-ledger spending balance before constructing a transaction. Their `BalanceRange` is the expected range of that **current balance**, not the amount being sent or converted back. + +The `Low` and `High` bounds are inclusive and must contain the plaintext balance. They must satisfy `Low <= High < math.MaxUint64`. Decryption searches the interval linearly, so use the narrowest practical range; unnecessarily large ranges can make transaction construction slow. Omitting `BalanceRange` produces `[0, 0]`, which only succeeds for a zero balance. + +`PrepareSend` and `PrepareConvertBack` do not decrypt ledger state because their `CurrentBalance` is supplied explicitly. + ### `BuildClawback` and `PrepareClawback` Use these for `ConfidentialMPTClawback`. @@ -167,4 +183,5 @@ Most builder errors are explicit and map to missing ledger state or invalid inpu - `ErrReceiverNotOptedIn`: the destination holder has no registered `HolderEncryptionKey`. - `ErrMPTokenNotFound`: the account does not yet have the expected `MPToken` ledger entry. - `ErrInsufficientBalance`: the requested confidential send or convert-back amount exceeds the decrypted balance. -- `ErrCryptoFailed`: a cryptographic primitive failed or the provided private key does not match ledger state. +- `elgamal.ErrInvalidAmountRange`: `BalanceRange` is inverted or its upper bound is `math.MaxUint64`. +- `ErrCryptoFailed`: a cryptographic primitive failed, the provided private key does not match ledger state, or the current balance falls outside `BalanceRange`. diff --git a/docs/docs/confidential/index.md b/docs/docs/confidential/index.md index 8a0623018..ce16879b4 100644 --- a/docs/docs/confidential/index.md +++ b/docs/docs/confidential/index.md @@ -34,7 +34,16 @@ Use this package when you need raw confidential amount encryption helpers. - `GenerateKeypair()` creates a confidential holder, issuer, or auditor keypair. - `GenerateBlindingFactor()` creates the shared randomness used across ciphertexts and commitments. - `Encrypt()` encrypts a `uint64` amount to a compressed secp256k1 public key. -- `Decrypt()` decrypts a confidential balance ciphertext with the matching private key. +- `Decrypt(ciphertextHex, privateKeyHex, amountRange)` decrypts a confidential balance ciphertext with the matching private key by searching an inclusive `AmountRange`. + +Decryption requires bounds that contain the plaintext amount and satisfy `Low <= High < math.MaxUint64`. Search cost grows linearly with the interval size, so use the narrowest practical range: + +```go +amount, err := elgamal.Decrypt(ciphertextHex, privateKeyHex, elgamal.AmountRange{ + Low: 0, + High: 1_000_000, +}) +``` ### `confidential/commitment` @@ -72,7 +81,7 @@ Related XRPL types were extended as well: Use [`builders`](/docs/confidential/builders) when you want the SDK to: - fetch ledger state such as `Sequence`, registered encryption keys, and confidential balance fields; -- decrypt the holder's current confidential balance when required; +- decrypt the holder's current confidential balance within a caller-supplied inclusive `BalanceRange` when required; - generate ciphertexts, commitments, and ZK proofs with the correct context hash; - return a ready-to-sign `xrpl/transaction` struct. diff --git a/pkg/hexutil/hexutil_test.go b/pkg/hexutil/hexutil_test.go index 2c5769ef7..32bd124ef 100644 --- a/pkg/hexutil/hexutil_test.go +++ b/pkg/hexutil/hexutil_test.go @@ -67,11 +67,17 @@ func TestDecodeFixedHex(t *testing.T) { wantErr: false, }, { - name: "fail - wrong byte length", + name: "fail - too short", hex: "0102", size: 32, wantErr: true, }, + { + name: "fail - too long", + hex: "010203", + size: 2, + wantErr: true, + }, { name: "fail - invalid hex chars", hex: "zzzz",