Skip to content

Commit

Permalink
use the common program derived address logic for address lookup table…
Browse files Browse the repository at this point in the history
… address derivation
  • Loading branch information
ml-james committed Jun 19, 2024
1 parent fcca28f commit 52a7400
Show file tree
Hide file tree
Showing 27 changed files with 197 additions and 250 deletions.
2 changes: 1 addition & 1 deletion src/main/java/com/lmax/solana4j/Solana.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public static PublicKey account(final byte[] bytes)

public static ProgramDerivedAddress programDerivedAddress(final PublicKey owner, final PublicKey programId)
{
return SolanaEncoding.programDerivedAddress(owner, programId);
return SolanaEncoding.deriveProgramAddress(owner, programId);
}

public static AssociatedTokenAddress associatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public interface AssociatedTokenAddress extends ProgramDerivedAddress
{
PublicKey mint();
PublicKey owner();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ public interface ProgramDerivedAddress
{
PublicKey address();

PublicKey owner();

PublicKey programId();

int nonce();
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/lmax/solana4j/api/PublicKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public interface PublicKey
int PUBLIC_KEY_LENGTH = 32;

String base58();
byte[] bytes();

void write(ByteBuffer buffer);
}
1 change: 1 addition & 0 deletions src/main/java/com/lmax/solana4j/api/Slot.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
public interface Slot
{
void write(ByteBuffer buffer);
byte[] bytes();
}
6 changes: 6 additions & 0 deletions src/main/java/com/lmax/solana4j/encoding/SolanaAccount.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ public String base58()
return Base58.encode(bytes);
}

@Override
public byte[] bytes()
{
return bytes;
}

@Override
public void write(final ByteBuffer buffer)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.lmax.solana4j.programs;
package com.lmax.solana4j.encoding;

import com.lmax.solana4j.api.AddressLookupTable;
import com.lmax.solana4j.api.PublicKey;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,58 +1,45 @@
package com.lmax.solana4j.encoding;

import com.lmax.solana4j.api.AssociatedTokenAddress;
import com.lmax.solana4j.api.ProgramDerivedAddress;
import com.lmax.solana4j.api.PublicKey;
import net.i2p.crypto.eddsa.math.Curve;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Objects;

import static com.lmax.solana4j.programs.AssociatedTokenProgram.ASSOCIATED_TOKEN_PROGRAM_ACCOUNT;
import static com.lmax.solana4j.programs.AssociatedTokenMetadataProgram.ASSOCIATED_TOKEN_PROGRAM_ACCOUNT;
import static java.util.Objects.requireNonNull;
import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.ED_25519_CURVE_SPEC;

final class SolanaAssociatedTokenAddress extends SolanaProgramDerivedAddress implements AssociatedTokenAddress
{
private final PublicKey mint;
private final PublicKey owner;

public static SolanaAssociatedTokenAddress deriveAssociatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount)
{
final Curve curve = ED_25519_CURVE_SPEC.getCurve();
int bumpSeed = 255;

byte[] programAddress = new byte[32];
try
{
while (bumpSeed > 0)
{
final ByteBuffer seeds = ByteBuffer.allocate(197);
owner.write(seeds);
tokenProgramAccount.write(seeds);
mint.write(seeds);
seeds.put(getNextSeed(bumpSeed));

final ByteBuffer programAccountBuf = ByteBuffer.allocate(32);
ASSOCIATED_TOKEN_PROGRAM_ACCOUNT.write(programAccountBuf);

programAddress = createProgramAddress(seeds, programAccountBuf);
curve.createPoint(programAddress, false);
bumpSeed--;
}
}
catch (final RuntimeException re)
{
return new SolanaAssociatedTokenAddress(new SolanaAccount(programAddress), owner, mint, tokenProgramAccount, bumpSeed);
}

throw new RuntimeException("Could not find a program address off the curve.");
final ProgramDerivedAddress programDerivedAddress =
SolanaProgramDerivedAddress.deriveProgramAddress(List.of(owner.bytes(), tokenProgramAccount.bytes(), mint.bytes()), ASSOCIATED_TOKEN_PROGRAM_ACCOUNT);
return new SolanaAssociatedTokenAddress(new SolanaAccount(programDerivedAddress.address().bytes()), owner, mint, tokenProgramAccount, programDerivedAddress.nonce());
}


SolanaAssociatedTokenAddress(final PublicKey address, final PublicKey owner, final PublicKey mint, final PublicKey programAccount, final int nonce)
{
super(address, owner, programAccount, nonce);

super(address, programAccount, nonce);
this.mint = requireNonNull(mint, "The mint public key must be specified, but was null");
this.owner = requireNonNull(owner, "The owner public key must be specified, but was null");
}

@Override
public PublicKey mint()
{
return mint;
}

@Override
public PublicKey owner()
{
return owner;
}

@Override
Expand All @@ -71,30 +58,24 @@ public boolean equals(final Object o)
return false;
}
final SolanaAssociatedTokenAddress that = (SolanaAssociatedTokenAddress) o;
return Objects.equals(mint, that.mint);
return Objects.equals(mint, that.mint) && Objects.equals(owner, that.owner);
}

@Override
public int hashCode()
{
return Objects.hash(super.hashCode(), mint);
return Objects.hash(super.hashCode(), mint, owner);
}

@Override
public String toString()
{
return "SolanaAssociatedTokenAddress{" +
"mint=" + mint +
", address=" + address +
", owner=" + owner +
", programId=" + programAccount +
", nonce=" + nonce +
'}';
}

@Override
public PublicKey mint()
{
return mint;
"mint=" + mint +
", address=" + address +
", owner=" + owner +
", programId=" + programAccount +
", nonce=" + nonce +
'}';
}
}
14 changes: 13 additions & 1 deletion src/main/java/com/lmax/solana4j/encoding/SolanaEncoding.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lmax.solana4j.encoding;

import com.lmax.solana4j.api.AddressLookupTable;
import com.lmax.solana4j.api.AssociatedTokenAddress;
import com.lmax.solana4j.api.Blockhash;
import com.lmax.solana4j.api.Destination;
Expand All @@ -12,6 +13,7 @@

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;

public final class SolanaEncoding
{
Expand All @@ -38,11 +40,16 @@ public static PublicKey account(final byte[] bytes)
return new SolanaAccount(bytes);
}

public static ProgramDerivedAddress programDerivedAddress(final PublicKey owner, final PublicKey programId)
public static ProgramDerivedAddress deriveProgramAddress(final PublicKey owner, final PublicKey programId)
{
return SolanaProgramDerivedAddress.deriveProgramAddress(owner, programId);
}

public static ProgramDerivedAddress deriveProgramAddress(final List<byte[]> seeds, final PublicKey programId)
{
return SolanaProgramDerivedAddress.deriveProgramAddress(seeds, programId);
}

public static AssociatedTokenAddress associatedTokenAddress(final PublicKey owner, final PublicKey mint, final PublicKey tokenProgramAccount)
{
return SolanaAssociatedTokenAddress.deriveAssociatedTokenAddress(owner, mint, tokenProgramAccount);
Expand All @@ -66,4 +73,9 @@ public static Destination destination(final PublicKey destination, final long am
{
return new SolanaDestination(destination, amount);
}

public static AddressLookupTable addressLookupTable(final PublicKey lookupTableAddress, final List<PublicKey> addressLookups)
{
return new SolanaAddressLookupTable(lookupTableAddress, addressLookups);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,80 +2,76 @@

import com.lmax.solana4j.api.ProgramDerivedAddress;
import com.lmax.solana4j.api.PublicKey;
import net.i2p.crypto.eddsa.math.Curve;
import net.i2p.crypto.eddsa.math.GroupElement;
import org.bitcoinj.core.Sha256Hash;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
import java.util.Objects;

import static com.lmax.solana4j.api.PublicKey.PUBLIC_KEY_LENGTH;
import static java.util.Objects.requireNonNull;
import static net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable.ED_25519_CURVE_SPEC;

class SolanaProgramDerivedAddress implements ProgramDerivedAddress
{
public static final byte[] PROGRAM_DERIVED_ADDRESS_BYTES = "ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8);
private static final int BUMP_LENGTH = 1;

final PublicKey address;
final PublicKey owner;
final PublicKey programAccount;
final int nonce;

// programId is just the Solana term for the public key of the program account
public static ProgramDerivedAddress deriveProgramAddress(final PublicKey owner, final PublicKey programId)
public static ProgramDerivedAddress deriveProgramAddress(final List<byte[]> seeds, final PublicKey programId)
{
final Curve curve = ED_25519_CURVE_SPEC.getCurve();
int bumpSeed = 255;
final int seedLength = seeds.stream().mapToInt(seed -> seed.length).sum();
final int byteLength = seedLength + PUBLIC_KEY_LENGTH + PROGRAM_DERIVED_ADDRESS_BYTES.length + BUMP_LENGTH;

byte[] programAddress = new byte[32];
try
int bumpSeed = 255;
while (bumpSeed > 0)
{
while (bumpSeed > 0)
{
final ByteBuffer seeds = ByteBuffer.allocate(33);
owner.write(seeds);
seeds.put(getNextSeed(bumpSeed));
final ByteBuffer seedsBuffer = ByteBuffer.allocate(byteLength);
seeds.forEach(seedsBuffer::put);
seedsBuffer.put(new byte[]{(byte) bumpSeed});

programId.write(seedsBuffer);
seedsBuffer.put(PROGRAM_DERIVED_ADDRESS_BYTES);

final ByteBuffer programIdBuf = ByteBuffer.allocate(32);
programId.write(programIdBuf);
final byte[] programAddress = Sha256Hash.hash(seedsBuffer.array());

programAddress = createProgramAddress(seeds, programIdBuf);
curve.createPoint(programAddress, false);
bumpSeed--;
if (isOffCurve(programAddress))
{
return new SolanaProgramDerivedAddress(new SolanaAccount(programAddress), programId, bumpSeed);
}
bumpSeed--;
}
catch (final RuntimeException re)
{
return new SolanaProgramDerivedAddress(new SolanaAccount(programAddress), owner, programId, bumpSeed);
}

throw new RuntimeException("Could not find a program address off the curve.");
}

/* protected */ static byte[] createProgramAddress(final ByteBuffer seeds, final ByteBuffer programAccount)
private static boolean isOffCurve(final byte[] programAddress)
{
final MessageDigest messageDigest = Sha256Hash.newDigest();

messageDigest.update(seeds.flip());
messageDigest.update(programAccount.flip());
messageDigest.update("ProgramDerivedAddress".getBytes(StandardCharsets.UTF_8));

return messageDigest.digest();
//is this the best way?
try
{
final GroupElement point = ED_25519_CURVE_SPEC.getCurve().createPoint(programAddress, false);
return !point.isOnCurve();
}
catch (final IllegalArgumentException e)
{
return true;
}
}

/* protected */ static byte[] getNextSeed(final int currentSignedByteValue)
// programId is just the Solana term for the public key of the program account
public static ProgramDerivedAddress deriveProgramAddress(final PublicKey owner, final PublicKey programId)
{
final byte[] seed = new byte[1];

seed[0] = (byte) (currentSignedByteValue);

return seed;
return deriveProgramAddress(List.of(owner.bytes()), programId);
}


SolanaProgramDerivedAddress(final PublicKey address, final PublicKey owner, final PublicKey programAccount, final int nonce)
SolanaProgramDerivedAddress(final PublicKey address, final PublicKey programAccount, final int nonce)
{
this.address = requireNonNull(address, "The address public key must be specified, but was null");
this.owner = requireNonNull(owner, "The owner public key must be specified, but was null");
this.programAccount = requireNonNull(programAccount, "The programId public key must be specified, but was null");
this.nonce = nonce;
}
Expand All @@ -92,24 +88,23 @@ public boolean equals(final Object o)
return false;
}
final SolanaProgramDerivedAddress that = (SolanaProgramDerivedAddress) o;
return nonce == that.nonce && Objects.equals(address, that.address) && Objects.equals(owner, that.owner) && Objects.equals(programAccount, that.programAccount);
return nonce == that.nonce && Objects.equals(address, that.address) && Objects.equals(programAccount, that.programAccount);
}

@Override
public int hashCode()
{
return Objects.hash(address, owner, programAccount, nonce);
return Objects.hash(address, programAccount, nonce);
}

@Override
public String toString()
{
return "SolanaProgramDerivedAccount{" +
"address=" + address +
", owner=" + owner +
", programAccount=" + programAccount +
", nonce=" + nonce +
'}';
"address=" + address +
", programAccount=" + programAccount +
", nonce=" + nonce +
'}';
}

@Override
Expand All @@ -118,12 +113,6 @@ public PublicKey address()
return address;
}

@Override
public PublicKey owner()
{
return owner;
}

@Override
public PublicKey programId()
{
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/lmax/solana4j/encoding/SolanaSlot.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public void write(final ByteBuffer buffer)
buffer.put(bytes);
}

@Override
public byte[] bytes()
{
return bytes;
}

@Override
public boolean equals(final Object o)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.lmax.solana4j.programs;
package com.lmax.solana4j.encoding;

import com.lmax.solana4j.Solana;
import com.lmax.solana4j.api.PublicKey;
Expand Down
Loading

0 comments on commit 52a7400

Please sign in to comment.