Add address derivation funcs

This commit is contained in:
Slavomir 2021-08-31 22:23:43 +02:00
parent 3853032c9c
commit deed41d82f
8 changed files with 334 additions and 38 deletions

View File

@ -3,7 +3,4 @@ package solana
const (
// There are 1-billion lamports in one SOL.
LAMPORTS_PER_SOL uint64 = 1000000000
// Maximum length of derived pubkey seed.
MAX_SEED_LENGTH uint8 = 32
)

1
go.mod
View File

@ -9,6 +9,7 @@ retract (
require (
contrib.go.opencensus.io/exporter/stackdriver v0.13.4 // indirect
filippo.io/edwards25519 v1.0.0-rc.1
github.com/AlekSi/pointer v1.1.0
github.com/GeertJohan/go.rice v1.0.0
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59

2
go.sum
View File

@ -27,6 +27,8 @@ contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/
contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc=
contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI=
github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=

174
keys.go
View File

@ -4,9 +4,13 @@ import (
"crypto"
"crypto/ed25519"
crypto_rand "crypto/rand"
"crypto/sha256"
"errors"
"fmt"
"io/ioutil"
"math"
"filippo.io/edwards25519"
"github.com/mr-tron/base58"
)
@ -80,7 +84,7 @@ func (k PrivateKey) PublicKey() PublicKey {
return publicKey
}
type PublicKey [32]byte
type PublicKey [PublicKeyLength]byte
func PublicKeyFromBytes(in []byte) (out PublicKey) {
byteCount := len(in)
@ -88,7 +92,7 @@ func PublicKeyFromBytes(in []byte) (out PublicKey) {
return
}
max := 32
max := PublicKeyLength
if byteCount < max {
max = byteCount
}
@ -111,8 +115,8 @@ func PublicKeyFromBase58(in string) (out PublicKey, err error) {
return out, fmt.Errorf("decode: %w", err)
}
if len(val) != 32 {
return out, fmt.Errorf("invalid length, expected 32, got %d", len(val))
if len(val) != PublicKeyLength {
return out, fmt.Errorf("invalid length, expected %v, got %d", PublicKeyLength, len(val))
}
copy(out[:], val)
@ -172,3 +176,165 @@ func (p PublicKey) IsZero() bool {
func (p PublicKey) String() string {
return base58.Encode(p[:])
}
type PublicKeySlice []PublicKey
// UniqueAppend appends the provided pubkey only if it is not
// already present in the slice.
// Returns true when the provided pubkey wasn't already present.
func (slice *PublicKeySlice) UniqueAppend(pubkey PublicKey) bool {
if !slice.Has(pubkey) {
slice.Append(pubkey)
return true
}
return false
}
func (slice *PublicKeySlice) Append(pubkey PublicKey) {
*slice = append(*slice, pubkey)
}
func (slice PublicKeySlice) Has(pubkey PublicKey) bool {
for _, key := range slice {
if key.Equals(pubkey) {
return true
}
}
return false
}
var nativeProgramIDs = PublicKeySlice{
BPFLoaderProgramID,
BPFLoaderDeprecatedProgramID,
FeatureProgramID,
ConfigProgramID,
StakeProgramID,
VoteProgramID,
Secp256k1ProgramID,
SystemProgramID,
SysVarClockPubkey,
SysVarEpochSchedulePubkey,
SysVarFeesPubkey,
SysVarInstructionsPubkey,
SysVarRecentBlockHashesPubkey,
SysVarRentPubkey,
SysVarRewardsPubkey,
SysVarSlotHashesPubkey,
SysVarSlotHistoryPubkey,
SysVarStakeHistoryPubkey,
}
// https://github.com/solana-labs/solana/blob/216983c50e0a618facc39aa07472ba6d23f1b33a/sdk/program/src/pubkey.rs#L372
func isNativeProgramID(key PublicKey) bool {
return nativeProgramIDs.Has(key)
}
const (
/// Number of bytes in a pubkey.
PublicKeyLength = 32
// Maximum length of derived pubkey seed.
MaxSeedLength = 32
// Maximum number of seeds.
MaxSeeds = 16
// // Maximum string length of a base58 encoded pubkey.
// MaxBase58Length = 44
)
// Ported from https://github.com/solana-labs/solana/blob/216983c50e0a618facc39aa07472ba6d23f1b33a/sdk/program/src/pubkey.rs#L159
func CreateWithSeed(base PublicKey, seed string, owner PublicKey) (PublicKey, error) {
if len(seed) > MaxSeedLength {
return PublicKey{}, errors.New("Max seed length exceeded")
}
// let owner = owner.as_ref();
// if owner.len() >= PDA_MARKER.len() {
// let slice = &owner[owner.len() - PDA_MARKER.len()..];
// if slice == PDA_MARKER {
// return Err(PubkeyError::IllegalOwner);
// }
// }
b := make([]byte, 0, 64+len(seed))
b = append(b, base[:]...)
b = append(b, seed[:]...)
b = append(b, owner[:]...)
hash := sha256.Sum256(b)
return PublicKeyFromBytes(hash[:]), nil
}
const PDA_MARKER = "ProgramDerivedAddress"
// 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")
}
for _, seed := range seeds {
if len(seed) > MaxSeedLength {
return PublicKey{}, errors.New("Max seed length exceeded")
}
}
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...)
}
buf = append(buf, programID[:]...)
buf = append(buf, []byte(PDA_MARKER)...)
hash := sha256.Sum256(buf)
_, err := new(edwards25519.Point).SetBytes(hash[:])
isOnCurve := err == nil
if isOnCurve {
return PublicKey{}, errors.New("invalid seeds; address must fall off the curve")
}
return PublicKeyFromBytes(hash[:]), nil
}
// Find a valid program address and its corresponding bump seed.
func FindProgramAddress(seed [][]byte, programID PublicKey) (PublicKey, uint8, error) {
var address PublicKey
var err error
bumpSeed := uint8(math.MaxUint8)
for bumpSeed != 0 {
address, err = CreateProgramAddress(append(seed, []byte{byte(bumpSeed)}), programID)
if err == nil {
return address, bumpSeed, nil
}
bumpSeed--
}
return PublicKey{}, bumpSeed, errors.New("unable to find a valid program address")
}
func FindAssociatedTokenAddress(
walletAddress PublicKey,
splTokenMintAddress PublicKey,
) (PublicKey, uint8, error) {
return findAssociatedTokenAddressAndBumpSeed(
walletAddress,
splTokenMintAddress,
SPLAssociatedTokenAccountProgramID,
)
}
func findAssociatedTokenAddressAndBumpSeed(
walletAddress PublicKey,
splTokenMintAddress PublicKey,
programID PublicKey,
) (PublicKey, uint8, error) {
return FindProgramAddress([][]byte{
walletAddress[:],
SPLTokenProgramID[:],
splTokenMintAddress[:],
},
programID,
)
}

View File

@ -140,3 +140,106 @@ func TestPublicKey_MarshalText(t *testing.T) {
payload,
)
}
func TestPublicKeySlice(t *testing.T) {
slice := make(PublicKeySlice, 0)
require.False(t, slice.Has(BPFLoaderProgramID))
slice.Append(BPFLoaderProgramID)
require.True(t, slice.Has(BPFLoaderProgramID))
require.Len(t, slice, 1)
slice.UniqueAppend(BPFLoaderProgramID)
require.Len(t, slice, 1)
slice.Append(ConfigProgramID)
require.Len(t, slice, 2)
require.True(t, slice.Has(ConfigProgramID))
}
func TestIsNativeProgramID(t *testing.T) {
require.True(t, isNativeProgramID(ConfigProgramID))
}
func TestCreateWithSeed(t *testing.T) {
{
got, err := CreateWithSeed(PublicKey{}, "limber chicken: 4/45", PublicKey{})
require.NoError(t, err)
require.True(t, got.Equals(MustPublicKeyFromBase58("9h1HyLCW5dZnBVap8C5egQ9Z6pHyjsh5MNy83iPqqRuq")))
}
}
func TestCreateProgramAddress(t *testing.T) {
program_id := MustPublicKeyFromBase58("BPFLoaderUpgradeab1e11111111111111111111111")
public_key := MustPublicKeyFromBase58("SeedPubey1111111111111111111111111111111111")
{
got, err := CreateProgramAddress([][]byte{
{},
{1},
},
program_id,
)
require.NoError(t, err)
require.True(t, got.Equals(MustPublicKeyFromBase58("BwqrghZA2htAcqq8dzP1WDAhTXYTYWj7CHxF5j7TDBAe")))
}
{
got, err := CreateProgramAddress([][]byte{
[]byte("☉"),
{0},
},
program_id,
)
require.NoError(t, err)
require.True(t, got.Equals(MustPublicKeyFromBase58("13yWmRpaTR4r5nAktwLqMpRNr28tnVUZw26rTvPSSB19")))
}
{
got, err := CreateProgramAddress([][]byte{
[]byte("Talking"),
[]byte("Squirrels"),
},
program_id,
)
require.NoError(t, err)
require.True(t, got.Equals(MustPublicKeyFromBase58("2fnQrngrQT4SeLcdToJAD96phoEjNL2man2kfRLCASVk")))
}
{
got, err := CreateProgramAddress([][]byte{
public_key[:],
{1},
},
program_id,
)
require.NoError(t, err)
require.True(t, got.Equals(MustPublicKeyFromBase58("976ymqVnfE32QFe6NfGDctSvVa36LWnvYxhU6G2232YL")))
}
}
// 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++ {
program_id := NewAccount().PrivateKey.PublicKey()
address, bump_seed, err := FindProgramAddress(
[][]byte{
[]byte("Lil'"),
[]byte("Bits"),
},
program_id,
)
require.NoError(t, err)
got, err := CreateProgramAddress(
[][]byte{
[]byte("Lil'"),
[]byte("Bits"),
[]byte{bump_seed},
},
program_id,
)
require.NoError(t, err)
require.Equal(t, address, got)
}
}

53
program_ids.go Normal file
View File

@ -0,0 +1,53 @@
package solana
var (
// Create new accounts, allocate account data, assign accounts to owning programs,
// transfer lamports from System Program owned accounts and pay transacation fees.
SystemProgramID = MustPublicKeyFromBase58("11111111111111111111111111111111")
// Add configuration data to the chain and the list of public keys that are permitted to modify it.
ConfigProgramID = MustPublicKeyFromBase58("Config1111111111111111111111111111111111111")
// Create and manage accounts representing stake and rewards for delegations to validators.
StakeProgramID = MustPublicKeyFromBase58("Stake11111111111111111111111111111111111111")
// Create and manage accounts that track validator voting state and rewards.
VoteProgramID = MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111")
BPFLoaderDeprecatedProgramID = MustPublicKeyFromBase58("BPFLoader1111111111111111111111111111111111")
// Deploys, upgrades, and executes programs on the chain.
BPFLoaderProgramID = MustPublicKeyFromBase58("BPFLoader2111111111111111111111111111111111")
BPFLoaderUpgradeableProgramID = MustPublicKeyFromBase58("BPFLoaderUpgradeab1e11111111111111111111111")
// Verify secp256k1 public key recovery operations (ecrecover).
Secp256k1ProgramID = MustPublicKeyFromBase58("KeccakSecp256k11111111111111111111111111111")
FeatureProgramID = MustPublicKeyFromBase58("Feature111111111111111111111111111111111111")
)
// SPL:
var (
// A Token program on the Solana blockchain.
// This program defines a common implementation for Fungible and Non Fungible tokens.
SPLTokenProgramID = MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
// A Uniswap-like exchange for the Token program on the Solana blockchain,
// implementing multiple automated market maker (AMM) curves.
SPLTokenSwapProgramID = MustPublicKeyFromBase58("SwaPpA9LAaLfeLi3a68M4DjnLqgtticKg6CnyNwgAC8")
SPLTokenSwapFeeOwner = MustPublicKeyFromBase58("HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN")
// A lending protocol for the Token program on the Solana blockchain inspired by Aave and Compound.
SPLTokenLendingProgramID = MustPublicKeyFromBase58("LendZqTs8gn5CTSJU1jWKhKuVpjJGom45nnwPb2AMTi")
// This program defines the convention and provides the mechanism for mapping
// the user's wallet address to the associated token accounts they hold.
SPLAssociatedTokenAccountProgramID = MustPublicKeyFromBase58("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
// The Memo program is a simple program that validates a string of UTF-8 encoded characters
// and verifies that any accounts provided are signers of the transaction.
// The program also logs the memo, as well as any verified signer addresses,
// to the transaction log, so that anyone can easily observe memos
// and know they were approved by zero or more addresses
// by inspecting the transaction log from a trusted provider.
SPLMemoProgramID = MustPublicKeyFromBase58("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
)

View File

@ -35,6 +35,9 @@ var (
// The Rent burn percentage is modified by manual feature activation.
SysVarRentPubkey = MustPublicKeyFromBase58("SysvarRent111111111111111111111111111111111")
//
SysVarRewardsPubkey = MustPublicKeyFromBase58("SysvarRewards111111111111111111111111111111")
// The SlotHashes sysvar contains the most recent hashes of the slot's parent banks.
// It is updated every slot.
SysVarSlotHashesPubkey = MustPublicKeyFromBase58("SysvarS1otHashes111111111111111111111111111")
@ -45,7 +48,4 @@ var (
// The StakeHistory sysvar contains the history of cluster-wide stake activations and de-activations per epoch.
// It is updated at the start of every epoch.
SysVarStakeHistoryPubkey = MustPublicKeyFromBase58("SysvarStakeHistory1111111111111111111111111")
//
SysVarRewardsPubkey = MustPublicKeyFromBase58("SysvarRewards111111111111111111111111111111")
)

View File

@ -36,32 +36,6 @@ func TransactionPayer(payer PublicKey) TransactionOption {
return transactionOptionFunc(func(opts *transactionOptions) { opts.payer = payer })
}
type pubkeySlice []PublicKey
// uniqueAppend appends the provided pubkey only if it is not
// already present in the slice.
// Returns true when the provided pubkey wasn't already present.
func (slice *pubkeySlice) uniqueAppend(pubkey PublicKey) bool {
if !slice.has(pubkey) {
slice.append(pubkey)
return true
}
return false
}
func (slice *pubkeySlice) append(pubkey PublicKey) {
*slice = append(*slice, pubkey)
}
func (slice *pubkeySlice) has(pubkey PublicKey) bool {
for _, key := range *slice {
if key.Equals(pubkey) {
return true
}
}
return false
}
var debugNewTransaction = false
type TransactionBuilder struct {
@ -134,13 +108,13 @@ func NewTransaction(instructions []Instruction, recentBlockHash Hash, opts ...Tr
}
}
programIDs := make(pubkeySlice, 0)
programIDs := make(PublicKeySlice, 0)
accounts := []*AccountMeta{}
for _, instruction := range instructions {
for _, key := range instruction.Accounts() {
accounts = append(accounts, key)
}
programIDs.uniqueAppend(instruction.ProgramID())
programIDs.UniqueAppend(instruction.ProgramID())
}
// Add programID to the account list