Skip to content

Solana adapter: deriveAddressAndPublicKey fails because getDerivedPublicKey collapses Ed25519 keys to SEC1 hex #31

@nikshepsvn

Description

@nikshepsvn

Description

`chainAdapters.solana.Solana.deriveAddressAndPublicKey()` throws `"Non-base58 character"` because the underlying `ChainSignatureContract.getDerivedPublicKey()` always converts its result through `K()` (uncompressed SEC1 hex), even for Ed25519 keys.

The Solana adapter then strips a non-existent `"ed25519:"` prefix from the hex string and base58-decodes the leftover hex:

async deriveAddressAndPublicKey(t, e) {
    let r = (await this.contract.getDerivedPublicKey({path: e, predecessor: t, IsEd25519: true}))
              .replace("ed25519:", "");
    let i = bs58.decode(r);   // ← decoding hex as base58 fails
    let a = new PublicKey(i);
    ...
}

Meanwhile `getDerivedPublicKey` does:

async getDerivedPublicKey(t) {
    let e = await this.provider.callFunction(this.contractId, "derived_public_key", {
        path: t.path, predecessor: t.predecessor, domain_id: t.IsEd25519 ? 1 : 0
    });
    return K(e)  // ← always converts to "04..." hex
}

The TypeScript signature claims Promise<UncompressedPubKeySEC1 | \Ed25519:${string}`>` but the implementation only returns the SEC1 form.

Reproduction

On mainnet, with `v1.signer` as the contract (which supports both domains):

import { contracts, chainAdapters } from "chainsig.js";
import { Connection } from "@solana/web3.js";

const contract = new contracts.ChainSignatureContract({
  contractId: "v1.signer",
  networkId: "mainnet",
});
const adapter = new chainAdapters.solana.Solana({
  solanaConnection: new Connection("https://api.mainnet-beta.solana.com"),
  contract,
});
await adapter.deriveAddressAndPublicKey("near", "solana-1");
// → Error: Non-base58 character

Verified by direct RPC call that `v1.signer.derived_public_key({domain_id: 1, ...})` returns a valid `"ed25519:"` string. The bug is purely in the chainsig.js conversion step.

Affected versions

`1.1.14` (latest at time of report).

Workaround

Bypass `getDerivedPublicKey` for Solana and call the MPC contract directly. The base58 part of `ed25519:` IS the Solana address:

const raw = await provider.callFunction(mpcContract, "derived_public_key", {
  path, predecessor, domain_id: 1,
}); // "ed25519:<base58>"
const address = raw.replace(/^ed25519:/, "");

Suggested fix

`getDerivedPublicKey` should branch on `IsEd25519` and return the raw `"ed25519:..."` string for Ed25519 keys (or return a discriminated union), then the Solana / Aptos / SUI adapters can consume it correctly.

Found while building near-hydra — happy to send a PR if helpful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions