diff --git a/keys.go b/keys.go index 114897fb..afd4943a 100644 --- a/keys.go +++ b/keys.go @@ -182,6 +182,11 @@ func (p PublicKey) Bytes() []byte { return []byte(p[:]) } +// Check if a `Pubkey` is on the ed25519 curve. +func (p PublicKey) IsOnCurve() bool { + return IsOnCurve(p[:]) +} + var zeroPublicKey = PublicKey{} // IsZero returns whether the public key is zero. @@ -281,23 +286,21 @@ func CreateWithSeed(base PublicKey, seed string, owner PublicKey) (PublicKey, er const PDA_MARKER = "ProgramDerivedAddress" +var ErrMaxSeedLengthExceeded = errors.New("Max seed length exceeded") + // Create a program address. // Ported from https://github.com/solana-labs/solana/blob/216983c50e0a618facc39aa07472ba6d23f1b33a/sdk/program/src/pubkey.rs#L204 func CreateProgramAddress(seeds [][]byte, programID PublicKey) (PublicKey, error) { if len(seeds) > MaxSeeds { - return PublicKey{}, errors.New("Max seed length exceeded") + return PublicKey{}, ErrMaxSeedLengthExceeded } for _, seed := range seeds { if len(seed) > MaxSeedLength { - return PublicKey{}, errors.New("Max seed length exceeded") + return PublicKey{}, ErrMaxSeedLengthExceeded } } - if isNativeProgramID(programID) { - return PublicKey{}, fmt.Errorf("illegal owner: %s is a native program", programID) - } - buf := []byte{} for _, seed := range seeds { buf = append(buf, seed...) @@ -307,15 +310,20 @@ func CreateProgramAddress(seeds [][]byte, programID PublicKey) (PublicKey, error buf = append(buf, []byte(PDA_MARKER)...) hash := sha256.Sum256(buf) - _, err := new(edwards25519.Point).SetBytes(hash[:]) - isOnCurve := err == nil - if isOnCurve { + if IsOnCurve(hash[:]) { return PublicKey{}, errors.New("invalid seeds; address must fall off the curve") } return PublicKeyFromBytes(hash[:]), nil } +// Check if the provided `b` is on the ed25519 curve. +func IsOnCurve(b []byte) bool { + _, err := new(edwards25519.Point).SetBytes(b) + isOnCurve := err == nil + return isOnCurve +} + // Find a valid program address and its corresponding bump seed. func FindProgramAddress(seed [][]byte, programID PublicKey) (PublicKey, uint8, error) { var address PublicKey diff --git a/keys_test.go b/keys_test.go index 5f057867..f810dfd0 100644 --- a/keys_test.go +++ b/keys_test.go @@ -18,6 +18,7 @@ package solana import ( + "encoding/binary" "encoding/hex" "errors" "testing" @@ -185,7 +186,8 @@ func TestCreateWithSeed(t *testing.T) { } } -func TestCreateProgramAddress(t *testing.T) { +func TestCreateProgramAddressFromRust(t *testing.T) { + // Ported from https://github.com/solana-labs/solana/blob/f32216588dfdbc7a7160c26331ce657a90f95ae7/sdk/program/src/pubkey.rs#L636 program_id := MustPublicKeyFromBase58("BPFLoaderUpgradeab1e11111111111111111111111") public_key := MustPublicKeyFromBase58("SeedPubey1111111111111111111111111111111111") @@ -234,6 +236,129 @@ func TestCreateProgramAddress(t *testing.T) { } } +func TestCreateProgramAddressFromTypescript(t *testing.T) { + t.Run( + "createProgramAddress", + // Ported from https://github.com/solana-labs/solana-web3.js/blob/168d5e088edd48f9f0c1a877e888592ca4cfdf38/test/publickey.test.ts#L113 + func(t *testing.T) { + program_id := MustPublicKeyFromBase58("BPFLoader1111111111111111111111111111111111") + public_key := MustPublicKeyFromBase58("SeedPubey1111111111111111111111111111111111") + + { + programAddress, err := CreateProgramAddress([][]byte{ + []byte(""), + {1}, + }, + program_id, + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(MustPublicKeyFromBase58("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT"))) + } + { + programAddress, err := CreateProgramAddress([][]byte{ + []byte("☉"), + }, + program_id, + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(MustPublicKeyFromBase58("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7"))) + } + { + programAddress, err := CreateProgramAddress([][]byte{ + []byte("Talking"), + []byte("Squirrels"), + }, + program_id, + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(MustPublicKeyFromBase58("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds"))) + } + { + programAddress, err := CreateProgramAddress([][]byte{ + public_key[:], + }, + program_id, + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(MustPublicKeyFromBase58("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K"))) + + { + programAddress2, err := CreateProgramAddress([][]byte{ + []byte("Talking"), + }, + program_id, + ) + require.NoError(t, err) + require.False(t, programAddress.Equals(programAddress2)) + } + } + { + _, err := CreateProgramAddress([][]byte{ + make([]byte, MaxSeedLength+1), + }, + program_id, + ) + require.EqualError(t, err, ErrMaxSeedLengthExceeded.Error()) + } + { + bn := make([]byte, 8) + binary.LittleEndian.PutUint64(bn, 2) + programAddress, err := CreateProgramAddress([][]byte{ + MustPublicKeyFromBase58("H4snTKK9adiU15gP22ErfZYtro3aqR9BTMXiH3AwiUTQ").Bytes(), + bn, + }, + MustPublicKeyFromBase58("4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn"), + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(MustPublicKeyFromBase58("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"))) + } + }, + ) + + t.Run( + "findProgramAddress", + // Ported from https://github.com/solana-labs/solana-web3.js/blob/168d5e088edd48f9f0c1a877e888592ca4cfdf38/test/publickey.test.ts#L194 + func(t *testing.T) { + programId := MustPublicKeyFromBase58("BPFLoader1111111111111111111111111111111111") + + programAddress, nonce, err := FindProgramAddress( + [][]byte{ + []byte(""), + }, + programId, + ) + require.NoError(t, err) + + { + got, err := CreateProgramAddress([][]byte{ + []byte(""), + {nonce}, + }, + programId, + ) + require.NoError(t, err) + require.True(t, programAddress.Equals(got)) + } + }, + ) + + t.Run( + "isOnCurve", + // Ported from https://github.com/solana-labs/solana-web3.js/blob/168d5e088edd48f9f0c1a877e888592ca4cfdf38/test/publickey.test.ts#L212 + func(t *testing.T) { + onCurve := NewWallet().PublicKey() + require.True(t, onCurve.IsOnCurve()) + + // A program address, yanked from one of the above tests. This is a pretty + // poor test vector since it was created by the same code it is testing. + // Unfortunately, I've been unable to find a golden negative example input + // for curve25519 point decompression :/ + offCurve := MustPublicKeyFromBase58("12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA") + require.False(t, offCurve.IsOnCurve()) + }, + ) +} + // https://github.com/solana-labs/solana/blob/216983c50e0a618facc39aa07472ba6d23f1b33a/sdk/program/src/pubkey.rs#L590 func TestFindProgramAddress(t *testing.T) { for i := 0; i < 1_000; i++ {