From 4c34c3ef5a729908eaa2edc9fde647b261ed1c0f Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 11:14:16 +0200 Subject: [PATCH 1/8] fix(confidential): restore bounded MPT decryption --- confidential/builder/clawback_test.go | 2 + confidential/builder/convert_back.go | 13 ++- confidential/builder/convert_back_test.go | 57 ++++++++++ confidential/builder/convert_test.go | 2 + confidential/builder/send.go | 15 ++- confidential/builder/send_test.go | 60 +++++++++++ confidential/commitment/commitment_test.go | 2 +- confidential/elgamal/elgamal.go | 32 +++++- confidential/elgamal/elgamal_nocgo_test.go | 21 ++++ confidential/elgamal/elgamal_test.go | 101 +++++++++++++++--- confidential/elgamal/errors.go | 2 + confidential/mptcrypto/README.md | 18 ++-- confidential/mptcrypto/errors.go | 26 +++++ confidential/mptcrypto/mptcrypto_cgo.go | 12 ++- confidential/mptcrypto/mptcrypto_nocgo.go | 7 +- .../mptcrypto/mptcrypto_nocgo_test.go | 35 ++++++ confidential/mptcrypto/mptcrypto_test.go | 67 +++++++----- confidential/proof/clawback_test.go | 2 +- confidential/proof/context_test.go | 2 +- confidential/proof/convert_back_test.go | 2 +- confidential/proof/convert_test.go | 2 +- confidential/proof/helpers_test.go | 2 +- confidential/proof/send_test.go | 2 +- confidential/proof/verify_test.go | 2 +- 24 files changed, 411 insertions(+), 75 deletions(-) create mode 100644 confidential/elgamal/elgamal_nocgo_test.go create mode 100644 confidential/mptcrypto/errors.go create mode 100644 confidential/mptcrypto/mptcrypto_nocgo_test.go 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..08f3a3abd 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 ( @@ -205,8 +207,63 @@ func TestBuildConvertBack_Pass(t *testing.T) { Amount: withdrawAmount, HolderPrivKey: holderKP.PrivKeyHex, HolderPubKey: holderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: currentBalance, High: currentBalance}, }) require.NoError(t, err) require.NotNil(t, result) require.Equal(t, uint32(3), result.Sequence) } + +func TestBuildConvertBack_FailBalanceOutsideRange(t *testing.T) { + holderKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + issuerKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + + const currentBalance uint64 = 1000 + + 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) + + q := &mockQuerier{ + accountSeq: 3, + entries: map[string]ledgerentries.FlatLedgerObject{ + issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), + mptokenIndex: buildMPTokenEntry(holderKP.PubKeyHex, balanceCt, 1, ""), + }, + } + + _, err = BuildConvertBack(q, BuildConvertBackParams{ + Account: testAccount, + IssuanceID: testIssuanceID, + Amount: 100, + HolderPrivKey: holderKP.PrivKeyHex, + HolderPubKey: holderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: 0, High: currentBalance - 1}, + }) + require.ErrorIs(t, err, ErrCryptoFailed) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) +} + +func TestBuildConvertBack_InvalidRangeBeforeLedgerQueries(t *testing.T) { + holderKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + + _, err = BuildConvertBack(&mockQuerier{accountErr: ErrLedgerQuery}, BuildConvertBackParams{ + Account: testAccount, + IssuanceID: testIssuanceID, + Amount: 1, + HolderPrivKey: holderKP.PrivKeyHex, + HolderPubKey: holderKP.PubKeyHex, + BalanceRange: elgamal.AmountRange{Low: 2, High: 1}, + }) + require.ErrorIs(t, err, elgamal.ErrInvalidAmountRange) + require.NotErrorIs(t, err, ErrLedgerQuery) +} 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/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..b54b4f6d1 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 ( @@ -308,6 +310,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 +318,62 @@ func TestBuildSend_Pass(t *testing.T) { require.NotEmpty(t, result.ZKProof) } +func TestBuildSend_FailBalanceOutsideRange(t *testing.T) { + senderKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + issuerKP, err := elgamal.GenerateKeypair() + require.NoError(t, err) + + const currentBalance uint64 = 1000 + + 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) + require.NoError(t, err) + + q := &mockQuerier{ + accountSeq: 8, + entries: map[string]ledgerentries.FlatLedgerObject{ + issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), + senderMPTIndex: buildMPTokenEntry(senderKP.PubKeyHex, senderBalanceCt, 2, ""), + }, + } + + _, 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) + + _, err = BuildSend(&mockQuerier{accountErr: ErrLedgerQuery}, 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) +} + func TestBuildSend_FailReceiverNotOptedIn(t *testing.T) { senderKP, err := elgamal.GenerateKeypair() require.NoError(t, err) @@ -348,6 +407,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_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..5001dbb84 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() @@ -63,15 +81,19 @@ func Encrypt(amount uint64, pubkeyHex, bfHex string) (string, error) { return hex.EncodeToString(ct[:]), nil } -// Decrypt decrypts a ciphertext using a private key. +// Decrypt decrypts a ciphertext using a private key by searching amountRange. // ciphertextHex: 132 hex chars (66 bytes), privkeyHex: 64 hex chars (32 bytes). -// Returns the plaintext uint64 amount. -func Decrypt(ciphertextHex, privkeyHex string) (uint64, error) { +// 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) } @@ -81,7 +103,7 @@ func Decrypt(ciphertextHex, privkeyHex string) (uint64, error) { copy(ct[:], ctBytes) copy(priv[:], privBytes) - result, err := mptcrypto.DecryptAmount(ct, priv) + result, err := mptcrypto.DecryptAmount(ct, priv, amountRange.Low, amountRange.High) if err != nil { return 0, fmt.Errorf("%w: %w", ErrDecryptFailed, err) } diff --git a/confidential/elgamal/elgamal_nocgo_test.go b/confidential/elgamal/elgamal_nocgo_test.go new file mode 100644 index 000000000..cb580ed6d --- /dev/null +++ b/confidential/elgamal/elgamal_nocgo_test.go @@ -0,0 +1,21 @@ +//go:build !cgo || js || wasip1 || tinygo || gofuzz || !(linux || darwin) || !(amd64 || arm64) + +package elgamal_test + +import ( + "strings" + "testing" + + "github.com/Peersyst/xrpl-go/confidential/elgamal" + "github.com/Peersyst/xrpl-go/confidential/mptcrypto" + "github.com/stretchr/testify/require" +) + +func TestDecryptWithoutCgo(t *testing.T) { + ciphertext := strings.Repeat("00", mptcrypto.CiphertextSize) + privateKey := strings.Repeat("00", mptcrypto.PrivKeySize) + + _, err := elgamal.Decrypt(ciphertext, privateKey, elgamal.AmountRange{Low: 0, High: 0}) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) + require.ErrorIs(t, err, mptcrypto.ErrCgoRequired) +} diff --git a/confidential/elgamal/elgamal_test.go b/confidential/elgamal/elgamal_test.go index 255f87f18..25aa8c5f9 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,45 @@ 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 TestDecryptOutsideRange(t *testing.T) { + kp, err := elgamal.GenerateKeypair() + require.NoError(t, err) + bf, err := elgamal.GenerateBlindingFactor() + require.NoError(t, err) + ciphertext, err := elgamal.Encrypt(42, kp.PubKeyHex, bf) + require.NoError(t, err) + + _, err = elgamal.Decrypt(ciphertext, kp.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 41}) + require.ErrorIs(t, err, elgamal.ErrDecryptFailed) +} + +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) @@ -111,7 +138,7 @@ func TestDecryptWithWrongKey(t *testing.T) { require.NoError(t, err) // Decrypting with a different private key should fail. - _, err = elgamal.Decrypt(ct, kp2.PrivKeyHex) + _, err = elgamal.Decrypt(ct, kp2.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 100}) require.ErrorIs(t, err, elgamal.ErrDecryptFailed) } @@ -151,7 +178,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, @@ -160,11 +203,37 @@ 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(ct, "short", elgamal.AmountRange{Low: 0, High: 1}) + return err + }, + wantErr: elgamal.ErrInvalidKey, + }, + { + name: "fail - decrypt short privkey", + fn: func() error { + ct, _ := elgamal.Encrypt(1, kp.PubKeyHex, bf) + _, err := elgamal.Decrypt(ct, 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 { + ct, _ := elgamal.Encrypt(1, kp.PubKeyHex, bf) + _, err := elgamal.Decrypt(ct, 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) { 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..48d47db5c 100644 --- a/confidential/mptcrypto/README.md +++ b/confidential/mptcrypto/README.md @@ -4,7 +4,7 @@ Go bindings for the [XRPLF/mpt-crypto](https://github.com/xrplf/mpt-crypto) C li ## Build requirements -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`. +CGo is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, argument validation still runs before otherwise valid calls return `ErrCgoRequired`. ```bash # normal build (CGo on by default) @@ -18,10 +18,12 @@ CGO_ENABLED=0 go test ./confidential/mptcrypto/... ``` 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) + types.go # Size constants, Participant, PedersenProofParams + errors.go # Shared errors, aliases, and range validation + mptcrypto_cgo.go # Real implementations (only built with CGo) + mptcrypto_nocgo.go # No-CGo fallbacks: validate, then return ErrCgoRequired + mptcrypto_test.go # CGo-backed tests + mptcrypto_nocgo_test.go # No-CGo contract tests ``` 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. @@ -105,12 +107,12 @@ ct, err := mptcrypto.EncryptAmount(1000, pubkey, blindingFactor) // ct: 66-byte ciphertext ``` -#### `DecryptAmount(ciphertext [66]byte, privkey [32]byte) (uint64, error)` +#### `DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, rangeHigh uint64) (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. +Decrypts an ElGamal ciphertext by searching the inclusive `[rangeLow, rangeHigh]` interval. Bounds must satisfy `rangeLow <= rangeHigh < math.MaxUint64`. Search cost grows linearly with the interval size, so use the narrowest practical bounds. ```go -amount, err := mptcrypto.DecryptAmount(ciphertext, privkey) +amount, err := mptcrypto.DecryptAmount(ciphertext, privateKey, 0, 10_000) ``` ### 2. Context hashes diff --git a/confidential/mptcrypto/errors.go b/confidential/mptcrypto/errors.go new file mode 100644 index 000000000..bdd298c72 --- /dev/null +++ b/confidential/mptcrypto/errors.go @@ -0,0 +1,26 @@ +package mptcrypto + +import ( + "errors" + "fmt" + "math" +) + +// Ciphertext is a fixed-size ElGamal ciphertext. +type Ciphertext = [CiphertextSize]byte + +// PrivateKey is a fixed-size ElGamal private key. +type PrivateKey = [PrivKeySize]byte + +// ErrInvalidAmountRange is returned when a decryption search range is invalid. +var ErrInvalidAmountRange = errors.New("mptcrypto: invalid amount range") + +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 +} diff --git a/confidential/mptcrypto/mptcrypto_cgo.go b/confidential/mptcrypto/mptcrypto_cgo.go index 7d9f58bc4..1b0e230e1 100644 --- a/confidential/mptcrypto/mptcrypto_cgo.go +++ b/confidential/mptcrypto/mptcrypto_cgo.go @@ -109,13 +109,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) diff --git a/confidential/mptcrypto/mptcrypto_nocgo.go b/confidential/mptcrypto/mptcrypto_nocgo.go index 0018c8146..509447694 100644 --- a/confidential/mptcrypto/mptcrypto_nocgo.go +++ b/confidential/mptcrypto/mptcrypto_nocgo.go @@ -30,8 +30,11 @@ 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 + } return 0, ErrCgoRequired } diff --git a/confidential/mptcrypto/mptcrypto_nocgo_test.go b/confidential/mptcrypto/mptcrypto_nocgo_test.go new file mode 100644 index 000000000..f658ffc45 --- /dev/null +++ b/confidential/mptcrypto/mptcrypto_nocgo_test.go @@ -0,0 +1,35 @@ +//go:build !cgo || js || wasip1 || tinygo || gofuzz || !(linux || darwin) || !(amd64 || arm64) + +package mptcrypto_test + +import ( + "math" + "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{}, 0, 0) + require.ErrorIs(t, err, mptcrypto.ErrCgoRequired) +} + +func TestDecryptAmountValidatesRangeWithoutCgo(t *testing.T) { + tests := []struct { + name string + rangeLow uint64 + rangeHigh uint64 + }{ + {name: "low exceeds high", rangeLow: 2, rangeHigh: 1}, + {name: "high is max uint64", rangeLow: 0, rangeHigh: math.MaxUint64}, + } + + 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) + require.NotErrorIs(t, err, mptcrypto.ErrCgoRequired) + }) + } +} diff --git a/confidential/mptcrypto/mptcrypto_test.go b/confidential/mptcrypto/mptcrypto_test.go index df7b1c3ec..c8c4b4a44 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 @@ -49,38 +49,57 @@ 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) { + 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) }) } } 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_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..a6d1ae21a 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 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_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_test.go b/confidential/proof/verify_test.go index 37a118391..5fd6a786e 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 From c27212df1d72516c24bca2148afeb07cd02a4908 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 15:22:34 +0200 Subject: [PATCH 2/8] refactor(confidential): simplify crypto API --- confidential/builder/convert_back_test.go | 56 ++-------------- confidential/builder/helpers_test.go | 34 ++++++++++ confidential/builder/send_test.go | 57 +++------------- confidential/commitment/commitment.go | 3 +- confidential/elgamal/elgamal.go | 16 ++--- confidential/elgamal/elgamal_test.go | 20 +++--- confidential/mptcrypto/README.md | 79 +++++++++++++---------- confidential/mptcrypto/errors.go | 6 -- confidential/mptcrypto/mptcrypto_cgo.go | 38 +++++------ confidential/mptcrypto/mptcrypto_nocgo.go | 38 +++++------ confidential/mptcrypto/mptcrypto_test.go | 30 ++++----- confidential/mptcrypto/types.go | 28 ++++++-- confidential/proof/clawback.go | 21 ++---- confidential/proof/convert.go | 15 ++--- confidential/proof/convert_back.go | 21 ++---- confidential/proof/helpers.go | 10 +-- confidential/proof/send.go | 24 +++---- confidential/proof/verify.go | 12 ++-- pkg/hexutil/hexutil_test.go | 8 ++- 19 files changed, 230 insertions(+), 286 deletions(-) diff --git a/confidential/builder/convert_back_test.go b/confidential/builder/convert_back_test.go index 08f3a3abd..51b0d54e9 100644 --- a/confidential/builder/convert_back_test.go +++ b/confidential/builder/convert_back_test.go @@ -8,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" ) @@ -175,32 +173,10 @@ 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) - 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) - - q := &mockQuerier{ - accountSeq: 3, - entries: map[string]ledgerentries.FlatLedgerObject{ - issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), - mptokenIndex: buildMPTokenEntry(holderKP.PubKeyHex, balanceCt, 1, ""), - }, - } - + holderKP, q := newBalanceLedgerFixture(t, 3, 1, currentBalance) result, err := BuildConvertBack(q, BuildConvertBackParams{ Account: testAccount, IssuanceID: testIssuanceID, @@ -215,32 +191,10 @@ func TestBuildConvertBack_Pass(t *testing.T) { } func TestBuildConvertBack_FailBalanceOutsideRange(t *testing.T) { - holderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - const currentBalance uint64 = 1000 - 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) - - q := &mockQuerier{ - accountSeq: 3, - entries: map[string]ledgerentries.FlatLedgerObject{ - issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), - mptokenIndex: buildMPTokenEntry(holderKP.PubKeyHex, balanceCt, 1, ""), - }, - } - - _, err = BuildConvertBack(q, BuildConvertBackParams{ + holderKP, q := newBalanceLedgerFixture(t, 3, 1, currentBalance) + _, err := BuildConvertBack(q, BuildConvertBackParams{ Account: testAccount, IssuanceID: testIssuanceID, Amount: 100, @@ -256,7 +210,8 @@ func TestBuildConvertBack_InvalidRangeBeforeLedgerQueries(t *testing.T) { holderKP, err := elgamal.GenerateKeypair() require.NoError(t, err) - _, err = BuildConvertBack(&mockQuerier{accountErr: ErrLedgerQuery}, BuildConvertBackParams{ + q := &mockQuerier{accountErr: ErrLedgerQuery} + _, err = BuildConvertBack(q, BuildConvertBackParams{ Account: testAccount, IssuanceID: testIssuanceID, Amount: 1, @@ -266,4 +221,5 @@ func TestBuildConvertBack_InvalidRangeBeforeLedgerQueries(t *testing.T) { }) 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/helpers_test.go b/confidential/builder/helpers_test.go index 2a61024b6..d1f1a6caa 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,11 @@ type mockQuerier struct { accountSeq uint32 accountErr error // when set, GetAccountInfo returns this error entries map[string]ledgerentries.FlatLedgerObject + queryCalls int } func (m *mockQuerier) GetAccountInfo(_ *account.InfoRequest) (*account.InfoResponse, error) { + m.queryCalls++ if m.accountErr != nil { return nil, m.accountErr } @@ -29,6 +36,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 +72,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_test.go b/confidential/builder/send_test.go index b54b4f6d1..72d11891d 100644 --- a/confidential/builder/send_test.go +++ b/confidential/builder/send_test.go @@ -272,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, @@ -319,32 +298,10 @@ func TestBuildSend_Pass(t *testing.T) { } func TestBuildSend_FailBalanceOutsideRange(t *testing.T) { - senderKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - issuerKP, err := elgamal.GenerateKeypair() - require.NoError(t, err) - const currentBalance uint64 = 1000 - 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) - require.NoError(t, err) - - q := &mockQuerier{ - accountSeq: 8, - entries: map[string]ledgerentries.FlatLedgerObject{ - issuanceIndex: buildIssuanceEntry(issuerKP.PubKeyHex, ""), - senderMPTIndex: buildMPTokenEntry(senderKP.PubKeyHex, senderBalanceCt, 2, ""), - }, - } - - _, err = BuildSend(q, BuildSendParams{ + senderKP, q := newBalanceLedgerFixture(t, 8, 2, currentBalance) + _, err := BuildSend(q, BuildSendParams{ Account: testAccount, Destination: testDestination, IssuanceID: testIssuanceID, @@ -361,7 +318,8 @@ func TestBuildSend_InvalidRangeBeforeLedgerQueries(t *testing.T) { senderKP, err := elgamal.GenerateKeypair() require.NoError(t, err) - _, err = BuildSend(&mockQuerier{accountErr: ErrLedgerQuery}, BuildSendParams{ + q := &mockQuerier{accountErr: ErrLedgerQuery} + _, err = BuildSend(q, BuildSendParams{ Account: testAccount, Destination: testDestination, IssuanceID: testIssuanceID, @@ -372,6 +330,7 @@ func TestBuildSend_InvalidRangeBeforeLedgerQueries(t *testing.T) { }) 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) { 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/elgamal/elgamal.go b/confidential/elgamal/elgamal.go index 5001dbb84..d4e89a84e 100644 --- a/confidential/elgamal/elgamal.go +++ b/confidential/elgamal/elgamal.go @@ -69,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 { @@ -82,7 +80,7 @@ func Encrypt(amount uint64, pubkeyHex, bfHex string) (string, error) { } // Decrypt decrypts a ciphertext using a private key by searching amountRange. -// ciphertextHex: 132 hex chars (66 bytes), privkeyHex: 64 hex chars (32 bytes). +// 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 { @@ -98,12 +96,10 @@ func Decrypt(ciphertextHex, privateKeyHex string, amountRange AmountRange) (uint 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, amountRange.Low, amountRange.High) + 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 25aa8c5f9..802968220 100644 --- a/confidential/elgamal/elgamal_test.go +++ b/confidential/elgamal/elgamal_test.go @@ -143,8 +143,12 @@ func TestDecryptWithWrongKey(t *testing.T) { } 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 @@ -202,8 +206,7 @@ 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", elgamal.AmountRange{Low: 0, High: 1}) + _, err := elgamal.Decrypt(ciphertext, "short", elgamal.AmountRange{Low: 0, High: 1}) return err }, wantErr: elgamal.ErrInvalidKey, @@ -211,8 +214,7 @@ func TestInvalidHexInputs(t *testing.T) { { name: "fail - decrypt short privkey", fn: func() error { - ct, _ := elgamal.Encrypt(1, kp.PubKeyHex, bf) - _, err := elgamal.Decrypt(ct, strings.Repeat("00", mptcrypto.PrivKeySize-1), elgamal.AmountRange{Low: 0, High: 1}) + _, err := elgamal.Decrypt(ciphertext, strings.Repeat("00", mptcrypto.PrivKeySize-1), elgamal.AmountRange{Low: 0, High: 1}) return err }, wantErr: elgamal.ErrInvalidKey, @@ -220,8 +222,7 @@ func TestInvalidHexInputs(t *testing.T) { { name: "fail - decrypt long privkey", fn: func() error { - ct, _ := elgamal.Encrypt(1, kp.PubKeyHex, bf) - _, err := elgamal.Decrypt(ct, strings.Repeat("00", mptcrypto.PrivKeySize+1), elgamal.AmountRange{Low: 0, High: 1}) + _, err := elgamal.Decrypt(ciphertext, strings.Repeat("00", mptcrypto.PrivKeySize+1), elgamal.AmountRange{Low: 0, High: 1}) return err }, wantErr: elgamal.ErrInvalidKey, @@ -237,8 +238,7 @@ func TestInvalidHexInputs(t *testing.T) { } 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/mptcrypto/README.md b/confidential/mptcrypto/README.md index 48d47db5c..c2c7f4d4a 100644 --- a/confidential/mptcrypto/README.md +++ b/confidential/mptcrypto/README.md @@ -4,13 +4,13 @@ Go bindings for the [XRPLF/mpt-crypto](https://github.com/xrplf/mpt-crypto) C li ## Build requirements -CGo is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, argument validation still runs before otherwise valid calls return `ErrCgoRequired`. +CGo is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, bounded-decryption range validation still runs before otherwise valid calls return `ErrCgoRequired`. ```bash # normal build (CGo on by default) go test ./confidential/mptcrypto/... -# force CGo off (all functions return ErrCgoRequired) +# force CGo off (exercise no-CGo fallbacks) CGO_ENABLED=0 go test ./confidential/mptcrypto/... ``` @@ -18,15 +18,15 @@ CGO_ENABLED=0 go test ./confidential/mptcrypto/... ``` mptcrypto/ - types.go # Size constants, Participant, PedersenProofParams - errors.go # Shared errors, aliases, and range validation + types.go # Size constants, defined value types, and proof types + errors.go # Shared errors and range validation mptcrypto_cgo.go # Real implementations (only built with CGo) - mptcrypto_nocgo.go # No-CGo fallbacks: validate, then return ErrCgoRequired + mptcrypto_nocgo.go # Range validation and ErrCgoRequired fallbacks mptcrypto_test.go # CGo-backed tests mptcrypto_nocgo_test.go # No-CGo contract tests ``` -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. +Every function uses **defined, fixed-size byte-array types** such as `PrivateKey`, `PublicKey`, and `Ciphertext`. These prevent callers from mixing semantically different values with the same underlying size. Hex encoding/decoding happens in the layers above (elgamal/, proof/, commitment/), never here. --- @@ -54,21 +54,34 @@ Every function works with **fixed-size byte arrays** (`[32]byte`, `[33]byte`, `[ | `SendProofSize` | 946 | Compact sigma + double bulletproof (192 + 754) | | `MaxParticipants` | 255 | Max participants in a send (C API uses uint8_t) | +### Value types + +```go +type PrivateKey [PrivKeySize]byte +type PublicKey [PubKeySize]byte +type BlindingFactor [BlindingFactorSize]byte +type Ciphertext [CiphertextSize]byte +type Commitment [CommitmentSize]byte +type ContextHash [HashOutputSize]byte +``` + +These are defined types rather than aliases, so Go rejects accidental substitutions such as passing a `BlindingFactor` where a `PrivateKey` is required. They enforce semantic separation, while the C library remains responsible for validating key and curve-point contents. + ### Structs ```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 + PubKey PublicKey + Ciphertext Ciphertext } // Parameters for generating Pedersen linkage proofs. type PedersenProofParams struct { - Commitment [CommitmentSize]byte // 33 bytes + Commitment Commitment Amount uint64 - Ciphertext [CiphertextSize]byte // 66 bytes - BlindingFactor [BlindingFactorSize]byte // 32 bytes + Ciphertext Ciphertext + BlindingFactor BlindingFactor } ``` @@ -80,7 +93,7 @@ type PedersenProofParams struct { These handle key generation, encryption, and decryption for confidential amounts. -#### `GenerateKeypair() (privkey [32]byte, pubkey [33]byte, err error)` +#### `GenerateKeypair() (privkey PrivateKey, pubkey PublicKey, err error)` Creates a new secp256k1 ElGamal keypair. @@ -90,7 +103,7 @@ priv, pub, err := mptcrypto.GenerateKeypair() // pub: 33-byte compressed public key (starts with 0x02 or 0x03) ``` -#### `GenerateBlindingFactor() (bf [32]byte, err error)` +#### `GenerateBlindingFactor() (bf BlindingFactor, err error)` Generates a cryptographically random 32-byte scalar. Used as the randomness parameter (`r`) when encrypting amounts or creating Pedersen commitments. @@ -98,7 +111,7 @@ Generates a cryptographically random 32-byte scalar. Used as the randomness para bf, err := mptcrypto.GenerateBlindingFactor() ``` -#### `EncryptAmount(amount uint64, pubkey [33]byte, bf [32]byte) (ct [66]byte, err error)` +#### `EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (ct Ciphertext, err error)` Encrypts an amount using ElGamal. The ciphertext is 66 bytes: two compressed EC points concatenated (C1 || C2). @@ -119,9 +132,9 @@ amount, err := mptcrypto.DecryptAmount(ciphertext, privateKey, 0, 10_000) 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. -All context hash functions return a `[32]byte` hash. +All context hash functions return a `ContextHash`. -#### `ConvertContextHash(account [20]byte, iss [24]byte, seq uint32) ([32]byte, error)` +#### `ConvertContextHash(account [20]byte, iss [24]byte, seq uint32) (ContextHash, error)` For **ConfidentialMPTConvert** transactions (public amount -> confidential). @@ -129,19 +142,19 @@ For **ConfidentialMPTConvert** transactions (public amount -> confidential). - `iss`: the 24-byte MPTokenIssuance ID - `seq`: the transaction sequence number -#### `ConvertBackContextHash(account [20]byte, iss [24]byte, seq, ver uint32) ([32]byte, error)` +#### `ConvertBackContextHash(account [20]byte, iss [24]byte, seq, ver uint32) (ContextHash, error)` For **ConfidentialMPTConvertBack** transactions (confidential -> public amount). Same as above plus `ver` (the version counter from the ledger object). -#### `SendContextHash(account [20]byte, iss [24]byte, seq uint32, dest [20]byte, ver uint32) ([32]byte, error)` +#### `SendContextHash(account [20]byte, iss [24]byte, seq uint32, dest [20]byte, ver uint32) (ContextHash, error)` For **ConfidentialMPTSend** transactions (confidential transfer between accounts). Adds `dest` (destination account ID) and `ver`. -#### `ClawbackContextHash(account [20]byte, iss [24]byte, seq uint32, holder [20]byte) ([32]byte, error)` +#### `ClawbackContextHash(account [20]byte, iss [24]byte, seq uint32, holder [20]byte) (ContextHash, error)` For **ConfidentialMPTClawback** transactions (issuer reclaims tokens from a holder). @@ -149,7 +162,7 @@ Adds `holder` (the account being clawed back from). ### 3. Pedersen commitment -#### `PedersenCommitment(amount uint64, bf [32]byte) (commitment [33]byte, err error)` +#### `PedersenCommitment(amount uint64, bf BlindingFactor) (commitment Commitment, err error)` 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). @@ -162,13 +175,13 @@ commitment, err := mptcrypto.PedersenCommitment(1000, blindingFactor) 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)` +#### `GenerateConvertProof(pubkey PublicKey, privkey PrivateKey, ctxHash ContextHash) ([64]byte, error)` **Schnorr proof of knowledge.** Proves you own the private key for the public key being registered, bound to the transaction via ctxHash. Used in: **ConfidentialMPTConvert** (registering a keypair on the ledger). -#### `GenerateConvertBackProof(privkey [32]byte, pubkey [33]byte, ctxHash [32]byte, amount uint64, params PedersenProofParams) ([816]byte, error)` +#### `GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) ([816]byte, error)` **Compact AND-composed sigma proof + single Bulletproof range proof.** Proves: 1. Your encrypted balance matches the Pedersen commitment (sigma proof over balance witness) @@ -176,13 +189,13 @@ Used in: **ConfidentialMPTConvert** (registering a keypair on the ledger). Used in: **ConfidentialMPTConvertBack**. -#### `GenerateClawbackProof(privkey [32]byte, pubkey [33]byte, ctxHash [32]byte, amount uint64, ciphertext [66]byte) ([64]byte, error)` +#### `GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) ([64]byte, error)` **Compact sigma proof.** Proves that the ciphertext decrypts to exactly the claimed amount, without revealing the private key. Used in: **ConfidentialMPTClawback** (issuer proves the amount they're clawing back matches the encrypted balance). -#### `GenerateSendProof(privkey [32]byte, pubkey [33]byte, amount uint64, participants []Participant, txBF [32]byte, ctxHash [32]byte, amountCommitment [33]byte, balanceParams PedersenProofParams) ([]byte, error)` +#### `GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error)` **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) @@ -198,19 +211,19 @@ Used in: **ConfidentialMPTSend**. These are the four main verifiers, one per transaction type. Each returns `nil` on success or an error on failure. -#### `VerifyConvertProof(proof [64]byte, pubkey [33]byte, ctxHash [32]byte) error` +#### `VerifyConvertProof(proof [64]byte, pubkey PublicKey, ctxHash ContextHash) error` Verifies the Schnorr proof from a ConfidentialMPTConvert. -#### `VerifyConvertBackProof(proof [816]byte, pubkey [33]byte, ciphertext [66]byte, balanceCommit [33]byte, amount uint64, ctxHash [32]byte) error` +#### `VerifyConvertBackProof(proof [816]byte, pubkey PublicKey, ciphertext Ciphertext, balanceCommit Commitment, amount uint64, ctxHash ContextHash) error` Verifies the compact sigma + range proof from a ConfidentialMPTConvertBack. -#### `VerifySendProof(proof []byte, participants []Participant, senderCt [66]byte, amountCommit, balanceCommit [33]byte, ctxHash [32]byte) error` +#### `VerifySendProof(proof []byte, participants []Participant, senderCt Ciphertext, amountCommit, balanceCommit Commitment, ctxHash ContextHash) error` Verifies the compact sigma + range proof from a ConfidentialMPTSend. -#### `VerifyClawbackProof(proof [64]byte, amount uint64, pubkey [33]byte, ciphertext [66]byte, ctxHash [32]byte) error` +#### `VerifyClawbackProof(proof [64]byte, amount uint64, pubkey PublicKey, ciphertext Ciphertext, ctxHash ContextHash) error` Verifies the compact sigma proof from a ConfidentialMPTClawback. @@ -218,17 +231,17 @@ Verifies the compact sigma proof from a ConfidentialMPTClawback. 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` +#### `VerifyRevealedAmount(amount uint64, bf BlindingFactor, 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` +#### `VerifySendRangeProof(proof [754]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) 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)` +#### `ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (Commitment, 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. @@ -264,7 +277,7 @@ The C functions expect raw `uint8_t*` pointers. Go arrays live in Go-managed mem ```go // Go side -var pubkey [33]byte +var pubkey PublicKey // Pass to C: "give me a *C.uint8_t pointing to pubkey[0]" C.some_c_function((*C.uint8_t)(unsafe.Pointer(&pubkey[0]))) @@ -349,4 +362,4 @@ if ret != 0 { ### The no-CGo build -`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. +`mptcrypto_nocgo.go` provides identical function signatures without linking the C library. `DecryptAmount` still validates its search range first; other calls return `ErrCgoRequired`. This lets the rest of the codebase compile and run tests for pure-Go packages without CGo. diff --git a/confidential/mptcrypto/errors.go b/confidential/mptcrypto/errors.go index bdd298c72..b13711549 100644 --- a/confidential/mptcrypto/errors.go +++ b/confidential/mptcrypto/errors.go @@ -6,12 +6,6 @@ import ( "math" ) -// Ciphertext is a fixed-size ElGamal ciphertext. -type Ciphertext = [CiphertextSize]byte - -// PrivateKey is a fixed-size ElGamal private key. -type PrivateKey = [PrivKeySize]byte - // ErrInvalidAmountRange is returned when a decryption search range is invalid. var ErrInvalidAmountRange = errors.New("mptcrypto: invalid amount range") diff --git a/confidential/mptcrypto/mptcrypto_cgo.go b/confidential/mptcrypto/mptcrypto_cgo.go index 1b0e230e1..585562104 100644 --- a/confidential/mptcrypto/mptcrypto_cgo.go +++ b/confidential/mptcrypto/mptcrypto_cgo.go @@ -71,7 +71,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 +83,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 +95,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]), @@ -134,7 +134,7 @@ func DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, range // 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), @@ -148,7 +148,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), @@ -163,7 +163,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), @@ -179,7 +179,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), @@ -198,7 +198,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]), @@ -215,7 +215,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]), @@ -231,7 +231,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]), @@ -248,7 +248,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]), @@ -265,7 +265,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") @@ -307,7 +307,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]), @@ -322,7 +322,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]), @@ -338,7 +338,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)) } @@ -368,7 +368,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), @@ -388,7 +388,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 @@ -410,7 +410,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]), @@ -428,7 +428,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 509447694..f79d1e5ec 100644 --- a/confidential/mptcrypto/mptcrypto_nocgo.go +++ b/confidential/mptcrypto/mptcrypto_nocgo.go @@ -14,18 +14,18 @@ var ErrCgoRequired = errors.New( // 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 } @@ -43,22 +43,22 @@ func DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, range // 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 } @@ -67,7 +67,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 } @@ -76,25 +76,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 } @@ -103,24 +103,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; // 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 } @@ -130,12 +130,12 @@ func VerifyClawbackProof(proof [CompactClawbackProofSize]byte, amount uint64, pu // 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 { +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 } @@ -144,7 +144,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_test.go b/confidential/mptcrypto/mptcrypto_test.go index c8c4b4a44..006b5f28b 100644 --- a/confidential/mptcrypto/mptcrypto_test.go +++ b/confidential/mptcrypto/mptcrypto_test.go @@ -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() @@ -107,7 +107,7 @@ func TestDecryptAmountInvalidRange(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) @@ -121,33 +121,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)) }, }, @@ -157,7 +157,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() @@ -366,9 +366,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))) 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/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/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/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/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/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", From bf91e1aae3a398f840eca5334d69ab8930368119 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 16:28:48 +0200 Subject: [PATCH 3/8] docs(confidential): document bounded decryption ranges --- docs/docs/confidential/builders.md | 23 ++++++++++++++++++++--- docs/docs/confidential/index.md | 13 +++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) 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. From 63aad6cc802bfe0d5d8fced91312261daacada50 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 16:47:15 +0200 Subject: [PATCH 4/8] fix(confidential): keep crypto unavailable without cgo --- confidential/elgamal/elgamal_nocgo_test.go | 21 ------------------ confidential/mptcrypto/README.md | 8 +++---- confidential/mptcrypto/errors.go | 11 ++++++++-- confidential/mptcrypto/mptcrypto_nocgo.go | 15 ++----------- .../mptcrypto/mptcrypto_nocgo_test.go | 22 +------------------ 5 files changed, 16 insertions(+), 61 deletions(-) delete mode 100644 confidential/elgamal/elgamal_nocgo_test.go diff --git a/confidential/elgamal/elgamal_nocgo_test.go b/confidential/elgamal/elgamal_nocgo_test.go deleted file mode 100644 index cb580ed6d..000000000 --- a/confidential/elgamal/elgamal_nocgo_test.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build !cgo || js || wasip1 || tinygo || gofuzz || !(linux || darwin) || !(amd64 || arm64) - -package elgamal_test - -import ( - "strings" - "testing" - - "github.com/Peersyst/xrpl-go/confidential/elgamal" - "github.com/Peersyst/xrpl-go/confidential/mptcrypto" - "github.com/stretchr/testify/require" -) - -func TestDecryptWithoutCgo(t *testing.T) { - ciphertext := strings.Repeat("00", mptcrypto.CiphertextSize) - privateKey := strings.Repeat("00", mptcrypto.PrivKeySize) - - _, err := elgamal.Decrypt(ciphertext, privateKey, elgamal.AmountRange{Low: 0, High: 0}) - require.ErrorIs(t, err, elgamal.ErrDecryptFailed) - require.ErrorIs(t, err, mptcrypto.ErrCgoRequired) -} diff --git a/confidential/mptcrypto/README.md b/confidential/mptcrypto/README.md index c2c7f4d4a..112030659 100644 --- a/confidential/mptcrypto/README.md +++ b/confidential/mptcrypto/README.md @@ -4,7 +4,7 @@ Go bindings for the [XRPLF/mpt-crypto](https://github.com/xrplf/mpt-crypto) C li ## Build requirements -CGo is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, bounded-decryption range validation still runs before otherwise valid calls return `ErrCgoRequired`. +CGo is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, no confidential cryptographic operation is available and every `mptcrypto` function returns `ErrCgoRequired` without processing its inputs. ```bash # normal build (CGo on by default) @@ -21,9 +21,9 @@ mptcrypto/ types.go # Size constants, defined value types, and proof types errors.go # Shared errors and range validation mptcrypto_cgo.go # Real implementations (only built with CGo) - mptcrypto_nocgo.go # Range validation and ErrCgoRequired fallbacks + mptcrypto_nocgo.go # ErrCgoRequired stubs for builds without CGo mptcrypto_test.go # CGo-backed tests - mptcrypto_nocgo_test.go # No-CGo contract tests + mptcrypto_nocgo_test.go # No-CGo availability contract ``` Every function uses **defined, fixed-size byte-array types** such as `PrivateKey`, `PublicKey`, and `Ciphertext`. These prevent callers from mixing semantically different values with the same underlying size. Hex encoding/decoding happens in the layers above (elgamal/, proof/, commitment/), never here. @@ -362,4 +362,4 @@ if ret != 0 { ### The no-CGo build -`mptcrypto_nocgo.go` provides identical function signatures without linking the C library. `DecryptAmount` still validates its search range first; other calls return `ErrCgoRequired`. This lets the rest of the codebase compile and run tests for pure-Go packages without CGo. +`mptcrypto_nocgo.go` provides identical function signatures without linking the C library. Every function immediately returns `ErrCgoRequired`; no argument validation or cryptographic work is performed. This lets applications handle unavailable confidential functionality as a normal error while the rest of the codebase continues to compile without CGo. diff --git a/confidential/mptcrypto/errors.go b/confidential/mptcrypto/errors.go index b13711549..e556a1b7d 100644 --- a/confidential/mptcrypto/errors.go +++ b/confidential/mptcrypto/errors.go @@ -6,8 +6,15 @@ import ( "math" ) -// ErrInvalidAmountRange is returned when a decryption search range is invalid. -var ErrInvalidAmountRange = errors.New("mptcrypto: invalid amount range") +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") +) func validateAmountRange(rangeLow, rangeHigh uint64) error { if rangeLow > rangeHigh { diff --git a/confidential/mptcrypto/mptcrypto_nocgo.go b/confidential/mptcrypto/mptcrypto_nocgo.go index f79d1e5ec..ea6a1f23c 100644 --- a/confidential/mptcrypto/mptcrypto_nocgo.go +++ b/confidential/mptcrypto/mptcrypto_nocgo.go @@ -2,14 +2,6 @@ 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. @@ -32,9 +24,6 @@ func EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (ct Ciphe // DecryptAmount decrypts a 66-byte ElGamal ciphertext using a private key. // 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 - } return 0, ErrCgoRequired } @@ -108,7 +97,7 @@ func VerifyConvertProof(proof [SchnorrProofSize]byte, pubkey PublicKey, ctxHash } // 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 PublicKey, ciphertext Ciphertext, balanceCommit Commitment, amount uint64, ctxHash ContextHash) error { return ErrCgoRequired @@ -129,7 +118,7 @@ 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. +// 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 } diff --git a/confidential/mptcrypto/mptcrypto_nocgo_test.go b/confidential/mptcrypto/mptcrypto_nocgo_test.go index f658ffc45..a25ec341d 100644 --- a/confidential/mptcrypto/mptcrypto_nocgo_test.go +++ b/confidential/mptcrypto/mptcrypto_nocgo_test.go @@ -3,7 +3,6 @@ package mptcrypto_test import ( - "math" "testing" "github.com/Peersyst/xrpl-go/confidential/mptcrypto" @@ -11,25 +10,6 @@ import ( ) func TestDecryptAmountWithoutCgo(t *testing.T) { - _, err := mptcrypto.DecryptAmount(mptcrypto.Ciphertext{}, mptcrypto.PrivateKey{}, 0, 0) + _, err := mptcrypto.DecryptAmount(mptcrypto.Ciphertext{}, mptcrypto.PrivateKey{}, 2, 1) require.ErrorIs(t, err, mptcrypto.ErrCgoRequired) } - -func TestDecryptAmountValidatesRangeWithoutCgo(t *testing.T) { - tests := []struct { - name string - rangeLow uint64 - rangeHigh uint64 - }{ - {name: "low exceeds high", rangeLow: 2, rangeHigh: 1}, - {name: "high is max uint64", rangeLow: 0, rangeHigh: math.MaxUint64}, - } - - 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) - require.NotErrorIs(t, err, mptcrypto.ErrCgoRequired) - }) - } -} From 4a1df39a6c08ebe1489ed1bc5bf4a90575297931 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 16:50:25 +0200 Subject: [PATCH 5/8] test(confidential): clarify mock query counter --- confidential/builder/helpers_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/confidential/builder/helpers_test.go b/confidential/builder/helpers_test.go index d1f1a6caa..87be8c086 100644 --- a/confidential/builder/helpers_test.go +++ b/confidential/builder/helpers_test.go @@ -22,6 +22,7 @@ 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 } From 528753baaf6404f7621880d7f49dfd757f8bbe26 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 17:29:09 +0200 Subject: [PATCH 6/8] test(confidentail): improve table tests --- confidential/builder/convert_back_test.go | 59 ++++++----- confidential/elgamal/elgamal_test.go | 38 +++---- confidential/mptcrypto/mptcrypto_test.go | 123 +++++++++++++--------- confidential/proof/convert_test.go | 33 +++--- confidential/proof/verify_test.go | 67 ++++-------- 5 files changed, 157 insertions(+), 163 deletions(-) diff --git a/confidential/builder/convert_back_test.go b/confidential/builder/convert_back_test.go index 51b0d54e9..f532d6466 100644 --- a/confidential/builder/convert_back_test.go +++ b/confidential/builder/convert_back_test.go @@ -172,38 +172,39 @@ func TestPrepareConvertBack_FailValidation(t *testing.T) { } } -func TestBuildConvertBack_Pass(t *testing.T) { +func TestBuildConvertBack_BalanceRange(t *testing.T) { const currentBalance uint64 = 1000 - const withdrawAmount uint64 = 100 - - holderKP, q := newBalanceLedgerFixture(t, 3, 1, currentBalance) - result, err := BuildConvertBack(q, BuildConvertBackParams{ - Account: testAccount, - IssuanceID: testIssuanceID, - Amount: withdrawAmount, - HolderPrivKey: holderKP.PrivKeyHex, - HolderPubKey: holderKP.PubKeyHex, - BalanceRange: elgamal.AmountRange{Low: currentBalance, High: currentBalance}, - }) - require.NoError(t, err) - require.NotNil(t, result) - require.Equal(t, uint32(3), result.Sequence) -} -func TestBuildConvertBack_FailBalanceOutsideRange(t *testing.T) { - const currentBalance uint64 = 1000 + 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}, + } - holderKP, q := newBalanceLedgerFixture(t, 3, 1, currentBalance) - _, err := BuildConvertBack(q, BuildConvertBackParams{ - Account: testAccount, - IssuanceID: testIssuanceID, - Amount: 100, - HolderPrivKey: holderKP.PrivKeyHex, - HolderPubKey: holderKP.PubKeyHex, - BalanceRange: elgamal.AmountRange{Low: 0, High: currentBalance - 1}, - }) - require.ErrorIs(t, err, ErrCryptoFailed) - require.ErrorIs(t, err, elgamal.ErrDecryptFailed) + 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) { diff --git a/confidential/elgamal/elgamal_test.go b/confidential/elgamal/elgamal_test.go index 802968220..5f596ed2a 100644 --- a/confidential/elgamal/elgamal_test.go +++ b/confidential/elgamal/elgamal_test.go @@ -75,18 +75,6 @@ func TestEncryptDecryptRoundtrip(t *testing.T) { } } -func TestDecryptOutsideRange(t *testing.T) { - kp, err := elgamal.GenerateKeypair() - require.NoError(t, err) - bf, err := elgamal.GenerateBlindingFactor() - require.NoError(t, err) - ciphertext, err := elgamal.Encrypt(42, kp.PubKeyHex, bf) - require.NoError(t, err) - - _, err = elgamal.Decrypt(ciphertext, kp.PrivKeyHex, elgamal.AmountRange{Low: 0, High: 41}) - require.ErrorIs(t, err, elgamal.ErrDecryptFailed) -} - func TestAmountRangeValidate(t *testing.T) { tests := []struct { name string @@ -126,20 +114,32 @@ 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, elgamal.AmountRange{Low: 0, High: 100}) - 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) { diff --git a/confidential/mptcrypto/mptcrypto_test.go b/confidential/mptcrypto/mptcrypto_test.go index 006b5f28b..67812bcdd 100644 --- a/confidential/mptcrypto/mptcrypto_test.go +++ b/confidential/mptcrypto/mptcrypto_test.go @@ -179,23 +179,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]) - - commit2, err := mptcrypto.PedersenCommitment(42, bf) - require.NoError(t, err) - require.Equal(t, commit, commit2) - }) - - 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) - }) + 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}, + } + + 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]) + + 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 @@ -204,25 +212,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) { @@ -378,7 +396,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) @@ -386,35 +405,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/proof/convert_test.go b/confidential/proof/convert_test.go index a6d1ae21a..2a992828e 100644 --- a/confidential/proof/convert_test.go +++ b/confidential/proof/convert_test.go @@ -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/verify_test.go b/confidential/proof/verify_test.go index 5fd6a786e..8707987af 100644 --- a/confidential/proof/verify_test.go +++ b/confidential/proof/verify_test.go @@ -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) { From 545743d47b941dc57ce141362ea37df73ebf1dc4 Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 17:42:29 +0200 Subject: [PATCH 7/8] refactor(confidential): relocate amount range validation --- confidential/mptcrypto/errors.go | 16 +--------------- confidential/mptcrypto/mptcrypto_cgo.go | 11 +++++++++++ 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/confidential/mptcrypto/errors.go b/confidential/mptcrypto/errors.go index e556a1b7d..eaddb8a1e 100644 --- a/confidential/mptcrypto/errors.go +++ b/confidential/mptcrypto/errors.go @@ -1,10 +1,6 @@ package mptcrypto -import ( - "errors" - "fmt" - "math" -) +import "errors" var ( // ErrCgoRequired is returned when native confidential MPT operations are unavailable. @@ -15,13 +11,3 @@ var ( // ErrInvalidAmountRange is returned when a decryption search range is invalid. ErrInvalidAmountRange = errors.New("mptcrypto: invalid amount range") ) - -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 -} diff --git a/confidential/mptcrypto/mptcrypto_cgo.go b/confidential/mptcrypto/mptcrypto_cgo.go index 585562104..b9c13a4d1 100644 --- a/confidential/mptcrypto/mptcrypto_cgo.go +++ b/confidential/mptcrypto/mptcrypto_cgo.go @@ -15,6 +15,7 @@ import "C" import ( "fmt" + "math" "unsafe" ) @@ -129,6 +130,16 @@ func DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, range 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 From 2d95a895504698a5180b5f0b6d1a58bbd887f6ed Mon Sep 17 00:00:00 2001 From: Kevin Pita Date: Tue, 28 Jul 2026 17:54:52 +0200 Subject: [PATCH 8/8] docs(confidential): update mptcrypto documentation --- confidential/mptcrypto/README.md | 454 ++++++++++------------- confidential/mptcrypto/mptcrypto_test.go | 1 + 2 files changed, 199 insertions(+), 256 deletions(-) diff --git a/confidential/mptcrypto/README.md b/confidential/mptcrypto/README.md index 112030659..b383b0e60 100644 --- a/confidential/mptcrypto/README.md +++ b/confidential/mptcrypto/README.md @@ -1,60 +1,80 @@ # 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 is required to run confidential cryptographic operations (`CGO_ENABLED=1`). The vendored C libraries live in `confidential/deps/libs//`. Without CGo, no confidential cryptographic operation is available and every `mptcrypto` function returns `ErrCgoRequired` without processing its inputs. +## Native backend availability + +The native implementation is selected only when all of the following are true: + +- 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. + +Vendored headers and static libraries live under `confidential/deps/`: + +| 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/` | + +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. ```bash -# normal build (CGo on by default) -go test ./confidential/mptcrypto/... +# Exercise the native implementation on a supported host. +go test ./confidential/mptcrypto -# force CGo off (exercise no-CGo fallbacks) -CGO_ENABLED=0 go test ./confidential/mptcrypto/... +# Exercise the fallback implementation. +CGO_ENABLED=0 go test ./confidential/mptcrypto ``` -## How this package is organized +## Package layout -``` +```text mptcrypto/ - types.go # Size constants, defined value types, and proof types - errors.go # Shared errors and range validation - mptcrypto_cgo.go # Real implementations (only built with CGo) - mptcrypto_nocgo.go # ErrCgoRequired stubs for builds without CGo - mptcrypto_test.go # CGo-backed tests - mptcrypto_nocgo_test.go # No-CGo availability contract + 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 ``` -Every function uses **defined, fixed-size byte-array types** such as `PrivateKey`, `PublicKey`, and `Ciphertext`. These prevent callers from mixing semantically different values with the same underlying size. Hex encoding/decoding happens in the layers above (elgamal/, proof/, commitment/), never here. - ---- - -## Types and constants +## Data model ### Size constants -| 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) | - -### Value types +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 type PrivateKey [PrivKeySize]byte @@ -65,18 +85,20 @@ type Commitment [CommitmentSize]byte type ContextHash [HashOutputSize]byte ``` -These are defined types rather than aliases, so Go rejects accidental substitutions such as passing a `BlindingFactor` where a `PrivateKey` is required. They enforce semantic separation, while the C library remains responsible for validating key and curve-point contents. +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`. -### Structs +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. + +### Compound inputs ```go -// A party in a confidential send (public key + their encrypted amount). +// One encrypted copy of a confidential send amount. type Participant struct { PubKey PublicKey Ciphertext Ciphertext } -// Parameters for generating Pedersen linkage proofs. +// A value represented by both an ElGamal ciphertext and a Pedersen commitment. type PedersenProofParams struct { Commitment Commitment Amount uint64 @@ -85,281 +107,201 @@ type PedersenProofParams struct { } ``` ---- +For send proofs, XLS-96 uses three participants, or four when an auditor is configured. Their order is part of the native proof contract: -## Function reference +1. sender +2. destination +3. issuer +4. optional auditor -### 1. ElGamal encryption +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. -These handle key generation, encryption, and decryption for confidential amounts. - -#### `GenerateKeypair() (privkey PrivateKey, pubkey PublicKey, err error)` - -Creates a new secp256k1 ElGamal keypair. +## Function reference -```go -priv, pub, err := mptcrypto.GenerateKeypair() -// priv: 32-byte private key -// pub: 33-byte compressed public key (starts with 0x02 or 0x03) -``` +### ElGamal -#### `GenerateBlindingFactor() (bf BlindingFactor, err error)` +#### `GenerateKeypair() (PrivateKey, PublicKey, error)` -Generates a cryptographically random 32-byte scalar. Used as the randomness parameter (`r`) when encrypting amounts or creating Pedersen commitments. +Generates a secp256k1 ElGamal keypair. The public key is a 33-byte compressed point. -```go -bf, err := mptcrypto.GenerateBlindingFactor() -``` +#### `GenerateBlindingFactor() (BlindingFactor, error)` -#### `EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (ct Ciphertext, err error)` +Generates a random scalar suitable for ElGamal encryption and Pedersen commitments. -Encrypts an amount using ElGamal. The ciphertext is 66 bytes: two compressed EC points concatenated (C1 || C2). +#### `EncryptAmount(amount uint64, pubkey PublicKey, bf BlindingFactor) (Ciphertext, error)` -```go -ct, err := mptcrypto.EncryptAmount(1000, pubkey, blindingFactor) -// ct: 66-byte ciphertext -``` +Encrypts `amount` under `pubkey` using `bf`. The result is the concatenation of two compressed EC points. #### `DecryptAmount(ciphertext Ciphertext, privateKey PrivateKey, rangeLow, rangeHigh uint64) (uint64, error)` -Decrypts an ElGamal ciphertext by searching the inclusive `[rangeLow, rangeHigh]` interval. Bounds must satisfy `rangeLow <= rangeHigh < math.MaxUint64`. Search cost grows linearly with the interval size, so use the narrowest practical bounds. +Searches for the plaintext in the inclusive interval `[rangeLow, rangeHigh]`. On a native build, the range must satisfy: -```go -amount, err := mptcrypto.DecryptAmount(ciphertext, privateKey, 0, 10_000) +```text +rangeLow <= rangeHigh < math.MaxUint64 ``` -### 2. Context hashes +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. -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. +### Transaction context hashes -All context hash functions return a `ContextHash`. - -#### `ConvertContextHash(account [20]byte, iss [24]byte, seq uint32) (ContextHash, error)` - -For **ConfidentialMPTConvert** transactions (public amount -> confidential). - -- `account`: the sender's 20-byte account ID -- `iss`: the 24-byte MPTokenIssuance ID -- `seq`: the transaction sequence number - -#### `ConvertBackContextHash(account [20]byte, iss [24]byte, seq, ver uint32) (ContextHash, error)` - -For **ConfidentialMPTConvertBack** transactions (confidential -> public amount). - -Same as above plus `ver` (the version counter from the ledger object). - -#### `SendContextHash(account [20]byte, iss [24]byte, seq uint32, dest [20]byte, ver uint32) (ContextHash, error)` - -For **ConfidentialMPTSend** transactions (confidential transfer between accounts). - -Adds `dest` (destination account ID) and `ver`. - -#### `ClawbackContextHash(account [20]byte, iss [24]byte, seq uint32, holder [20]byte) (ContextHash, error)` - -For **ConfidentialMPTClawback** transactions (issuer reclaims tokens from a holder). - -Adds `holder` (the account being clawed back from). - -### 3. Pedersen commitment - -#### `PedersenCommitment(amount uint64, bf BlindingFactor) (commitment Commitment, err error)` - -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 PublicKey, privkey PrivateKey, ctxHash ContextHash) ([64]byte, error)` - -**Schnorr proof of knowledge.** Proves you own the private key for the public key being registered, bound to the transaction via ctxHash. - -Used in: **ConfidentialMPTConvert** (registering a keypair on the ledger). - -#### `GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) ([816]byte, error)` +The fields correspond to the relevant XLS-96 transaction: -**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) +- 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: **ConfidentialMPTConvertBack**. +### Pedersen commitments -#### `GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) ([64]byte, error)` +#### `PedersenCommitment(amount uint64, bf BlindingFactor) (Commitment, error)` -**Compact sigma proof.** Proves that the ciphertext decrypts to exactly the claimed amount, without revealing the private key. +Computes a compressed Pedersen commitment to `amount` using `bf`. The operation is deterministic for the same amount and blinding factor. -Used in: **ConfidentialMPTClawback** (issuer proves the amount they're clawing back matches the encrypted balance). - -#### `GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error)` - -**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] - -Returns a fixed-size byte slice of `SendProofSize` (946) bytes. - -Used in: **ConfidentialMPTSend**. - -### 5. Proof verification (top-level) - -These are the four main verifiers, one per transaction type. Each returns `nil` on success or an error on failure. - -#### `VerifyConvertProof(proof [64]byte, pubkey PublicKey, ctxHash ContextHash) error` - -Verifies the Schnorr proof from a ConfidentialMPTConvert. - -#### `VerifyConvertBackProof(proof [816]byte, pubkey PublicKey, ciphertext Ciphertext, balanceCommit Commitment, amount uint64, ctxHash ContextHash) error` - -Verifies the compact sigma + range proof from a ConfidentialMPTConvertBack. - -#### `VerifySendProof(proof []byte, participants []Participant, senderCt Ciphertext, amountCommit, balanceCommit Commitment, ctxHash ContextHash) error` +#### `ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (Commitment, error)` -Verifies the compact sigma + range proof from a ConfidentialMPTSend. +Subtracts the transparent amount from a balance commitment and returns the commitment to the convert-back remainder. -#### `VerifyClawbackProof(proof [64]byte, amount uint64, pubkey PublicKey, ciphertext Ciphertext, ctxHash ContextHash) error` +### Proof generation -Verifies the compact sigma proof from a ConfidentialMPTClawback. +#### `GenerateConvertProof(pubkey PublicKey, privkey PrivateKey, ctxHash ContextHash) ([SchnorrProofSize]byte, error)` -### 6. Proof verification (internal components) +Generates the Schnorr proof of private-key knowledge used when a `ConfidentialMPTConvert` transaction registers a holder encryption key. -These verify individual pieces of a send proof. Useful for debugging or testing each component in isolation. +#### `GenerateConvertBackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, params PedersenProofParams) ([ConvertBackProofSize]byte, error)` -#### `VerifyRevealedAmount(amount uint64, bf BlindingFactor, holder, issuer Participant, auditor *Participant) error` +Generates an 816-byte proof containing: -Verifies that a plaintext amount and blinding factor are consistent with the participants' ciphertexts. `auditor` can be `nil` if there's no auditor. +- 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 -#### `VerifySendRangeProof(proof [754]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) error` +`params` describes the holder's original spending balance, ciphertext, commitment, and commitment blinding factor. -Verifies a double bulletproof: both the transfer amount and remaining balance are in [0, 2^64-1]. +#### `GenerateClawbackProof(privkey PrivateKey, pubkey PublicKey, ctxHash ContextHash, amount uint64, ciphertext Ciphertext) ([CompactClawbackProofSize]byte, error)` -### 7. Utilities - -#### `ComputeConvertBackRemainder(commitmentIn Commitment, amount uint64) (Commitment, 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. -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. +#### `GenerateSendProof(privkey PrivateKey, pubkey PublicKey, amount uint64, participants []Participant, txBF BlindingFactor, ctxHash ContextHash, amountCommitment Commitment, balanceParams PedersenProofParams) ([]byte, error)` -```go -remainder, err := mptcrypto.ComputeConvertBackRemainder(balanceCommitment, 500) -``` +Generates the 946-byte `ConfidentialMPTSend` proof: ---- +- 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 -## CGo patterns used in this package +The inputs have the following relationships: -If you need to modify or extend the bindings, here's how the CGo boundary works. +- `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. -### The preamble +The successful native call currently writes `SendProofSize` bytes. The slice return type mirrors the C API's output buffer plus output-length contract. -At the top of `mptcrypto_cgo.go`: +### Top-level verification ```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" +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 ``` -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). +Each verifier returns `nil` only when the native proof check succeeds. Additional input contracts: -### Passing byte arrays to C with unsafe.Pointer +- `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. -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: +### Auxiliary verification -```go -// Go side -var pubkey PublicKey - -// Pass to C: "give me a *C.uint8_t pointing to pubkey[0]" -C.some_c_function((*C.uint8_t)(unsafe.Pointer(&pubkey[0]))) -``` - -**What's happening step by step:** - -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 +#### `VerifyRevealedAmount(amount uint64, bf BlindingFactor, holder, issuer Participant, auditor *Participant) error` -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) +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. -### Converting Go structs to C structs +#### `VerifySendRangeProof(proof [DoubleBulletproofSize]byte, amountCommit, balanceCommitment Commitment, ctxHash ContextHash) error` -For complex types (account IDs, participants, proof params), we use helper functions that copy field-by-field: +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. -```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. +Other validation and native-library failures are returned as descriptive errors. Every native wrapper treats a non-zero C return code as failure. -### Error handling - -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` provides identical function signatures without linking the C library. Every function immediately returns `ErrCgoRequired`; no argument validation or cryptographic work is performed. This lets applications handle unavailable confidential functionality as a normal error while the rest of the codebase continues to compile without CGo. +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/mptcrypto_test.go b/confidential/mptcrypto/mptcrypto_test.go index 67812bcdd..f06876666 100644 --- a/confidential/mptcrypto/mptcrypto_test.go +++ b/confidential/mptcrypto/mptcrypto_test.go @@ -50,6 +50,7 @@ func TestGenerateBlindingFactor(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()