Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ERROR_CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ SDK 1 single-account programs use 10–14 for their input checks
| 2004 | SHA-256 or Keccak-256 result not 32 bytes |
| 2005 | Account index out of range |
| 2006 | Malformed CPI account metas, or more than 64 |
| 2007 | Malformed signer seeds or PDA input, or more than 4 signer groups |
| 2007 | Malformed signer seeds or PDA input, or more than 4 signer groups (`FindAddress`: also more than 15 seeds or an output that is not 33 bytes) |
| 2008 | CPI instruction data longer than 10,240 bytes |
| 2009 | Native callback missing (native tests only) |
| 2010 | Resize or original length beyond limits |
Expand Down
23 changes: 19 additions & 4 deletions docs/SDK2.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,29 @@ of at most 32 bytes each, including any bump. `Address` derives an address using
an explicit program key; `Matches` compares it with a context account key.
`FindAddress(c, program)` is Solana's `find_program_address`: it appends each
bump from 255 down to 1 (never 0, as in Solana) and returns the first
off-curve address with its bump, using one create-program-address syscall
(about 1,570 CU) per attempt, so on-curve results are skipped exactly. The
off-curve address with its bump, so on-curve results are skipped exactly. It
is one `solana.FindAddress` call (below), 1,500 CU per bump tried. The
builder is unchanged; add the bump for signing. A builder with 16 seeds has
no room for the bump (3001), no bump found is 3004, and other syscall statuses
propagate. Store the bump and use `Address` where it is known; see
no room for the bump (3001), no bump found is 3004, and other statuses (a
missing native callback) propagate. Store the bump and use `Address` where it is known; see
[examples/find-address](../examples/find-address/README.md). The host
`fixture.FindProgramAddress` now also stops at bump 1.

`solana.FindAddress(c, seeds, program, output)` is `find_program_address`: one
`sol_try_find_program_address` syscall (fogo-v0 by name, solana-v3 hash
`0x48504a38`) tries bumps 255 down to 1 and writes the first off-curve
address and its bump into the 33-byte `output`. `seeds` is one encoded group
without the bump, so at most 15 seeds of at most 32 bytes (`pda.Seeds.Bytes`
before `AddByte(bump)`). It returns 0, the runtime's 1 when no bump works
(output unchanged), or 2007 for a malformed group or wrong lengths, checked
before the syscall: the runtime aborts on more than 16 seeds or a seed over 32
bytes, and with 16 it would charge for all 255 bumps. The runtime charges
1,500 CU per bump tried, the same as one `CreateAddress`, so the saving over a
`CreateAddress` loop is its per-try seed parsing and its setup: 373 to 1,228
CU per search, 125 CU per extra try for three seeds
([measurements](../results/sdk/2026-10-19-find-address/README.md)).
Native contexts run the same search through `DeriveAddress`.

`cpi.Metas.Add(index, writable, signer)` builds at most 64 account metas
(`cpi.MaxMetas`); repeated accounts count. `cpi.Signers.Add(&seeds)` copies at
most four seed groups (`cpi.MaxSigners`) into a 1,024-byte buffer; the combined
Expand Down
33 changes: 15 additions & 18 deletions examples/escrow/.gosvm/sdk/sdk/pda/pda.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,27 @@ func (s *Seeds) Address(c solana.Context, program [32]byte) ([32]byte, uint64) {
// gives for these seeds under program: each bump from 255 down to 1 (never 0,
// as in Solana) is appended in turn and the first off-curve address is
// returned. The builder is unchanged; add the bump to it for signing. A
// builder with 16 seeds has no room for the bump (ErrSeeds). Each attempt is
// one create-program-address syscall (1,500 CU), so a bump of b costs about
// builder with 16 seeds has no room for the bump (ErrSeeds). The search is
// one solana.FindAddress call (sol_try_find_program_address), which the
// runtime charges 1,500 CU per bump tried, so a bump of b costs about
// (256-b)*1,500 CU; store the bump and use Address when it is known.
func (s *Seeds) FindAddress(c solana.Context, program [32]byte) ([32]byte, byte, uint64) {
var address [32]byte
probe := *s
if probe.AddByte(255) != 0 {
// Room for the one-byte bump seed, as AddByte checks.
if s.count >= 16 || s.used > 526 {
return address, 0, ErrSeeds
}
seeds := probe.Bytes()
last := uint64(len(seeds)) - 1
for bump := uint64(255); bump > 0; bump-- {
seeds[last] = byte(bump)
code := solana.CreateAddress(c, seeds, program[:], address[:])
if code == 0 {
return address, byte(bump), 0
}
// The syscall returns 1 for an on-curve address; other statuses
// (malformed input, a missing native callback) end the search.
if code != 1 {
return address, 0, code
}
var found [33]byte
code := solana.FindAddress(c, s.Bytes(), program[:], found[:])
if code == 1 {
return address, 0, ErrNoAddress
}
// Other statuses (a missing native callback) end the search.
if code != 0 {
return address, 0, code
}
return address, 0, ErrNoAddress
copy(address[:], found[:32])
return address, found[32], 0
}

func (s *Seeds) Matches(c solana.Context, index uint64, program [32]byte) (bool, uint64) {
Expand Down
39 changes: 39 additions & 0 deletions examples/escrow/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,45 @@ func validSignerSeeds(b []byte) bool {
return off == len(b)
}

// FindAddress is Pubkey::find_program_address: it tries bumps 255 down to 1
// and returns the first off-curve address of seeds plus that one bump byte.
// seeds is one encoded seed group without the bump, so at most 15 seeds.
// output is exactly 33 bytes: the address, then the bump. It returns 0 and
// fills output, 1 when no bump works (output unchanged), 2007 for malformed
// input, or another nonzero status from the derivation. On chain it is one
// sol_try_find_program_address syscall, which charges the same 1,500 CU per
// bump tried as one CreateAddress.
func FindAddress(c Context, seeds, program, output []byte) uint64 {
if len(program) != 32 || len(output) != 33 || len(seeds) > 496 {
return 2007
}
off, ok := validSeedGroup(seeds, 0)
if !ok || off != len(seeds) || seeds[0] > 15 {
return 2007
}
if c.DeriveAddress == nil {
return 2009
}
group := make([]byte, len(seeds)+2)
copy(group, seeds)
group[0]++
group[len(seeds)] = 1
var address [32]byte
for bump := 255; bump >= 1; bump-- {
group[len(seeds)+1] = byte(bump)
code := c.DeriveAddress(group, program, address[:])
if code == 0 {
copy(output, address[:])
output[32] = byte(bump)
return 0
}
if code != 1 {
return code
}
}
return 1
}

// CreateAddress derives an off-curve PDA with an explicit derivation program.
// seeds is one encoded seed group, normally produced by sdk/pda.Seeds.Bytes.
// The output is exactly 32 bytes; the syscall's exact nonzero status propagates.
Expand Down
2 changes: 1 addition & 1 deletion examples/escrow/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "f66ccbb30ab2be273f8bcedc30e8f850e2c061d9414f1c081997d1d1b6572c59",
"sdk_sha256": "6e97ac4bb6571bc2d44438fc0a1afe263f678034678e475356660f2301ca503f",
"sdk_version": 2,
"imports": {
"model": "example.org/gosvm/escrow/model"
Expand Down
33 changes: 15 additions & 18 deletions examples/fee-vault/.gosvm/sdk/sdk/pda/pda.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,27 @@ func (s *Seeds) Address(c solana.Context, program [32]byte) ([32]byte, uint64) {
// gives for these seeds under program: each bump from 255 down to 1 (never 0,
// as in Solana) is appended in turn and the first off-curve address is
// returned. The builder is unchanged; add the bump to it for signing. A
// builder with 16 seeds has no room for the bump (ErrSeeds). Each attempt is
// one create-program-address syscall (1,500 CU), so a bump of b costs about
// builder with 16 seeds has no room for the bump (ErrSeeds). The search is
// one solana.FindAddress call (sol_try_find_program_address), which the
// runtime charges 1,500 CU per bump tried, so a bump of b costs about
// (256-b)*1,500 CU; store the bump and use Address when it is known.
func (s *Seeds) FindAddress(c solana.Context, program [32]byte) ([32]byte, byte, uint64) {
var address [32]byte
probe := *s
if probe.AddByte(255) != 0 {
// Room for the one-byte bump seed, as AddByte checks.
if s.count >= 16 || s.used > 526 {
return address, 0, ErrSeeds
}
seeds := probe.Bytes()
last := uint64(len(seeds)) - 1
for bump := uint64(255); bump > 0; bump-- {
seeds[last] = byte(bump)
code := solana.CreateAddress(c, seeds, program[:], address[:])
if code == 0 {
return address, byte(bump), 0
}
// The syscall returns 1 for an on-curve address; other statuses
// (malformed input, a missing native callback) end the search.
if code != 1 {
return address, 0, code
}
var found [33]byte
code := solana.FindAddress(c, s.Bytes(), program[:], found[:])
if code == 1 {
return address, 0, ErrNoAddress
}
// Other statuses (a missing native callback) end the search.
if code != 0 {
return address, 0, code
}
return address, 0, ErrNoAddress
copy(address[:], found[:32])
return address, found[32], 0
}

func (s *Seeds) Matches(c solana.Context, index uint64, program [32]byte) (bool, uint64) {
Expand Down
39 changes: 39 additions & 0 deletions examples/fee-vault/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,45 @@ func validSignerSeeds(b []byte) bool {
return off == len(b)
}

// FindAddress is Pubkey::find_program_address: it tries bumps 255 down to 1
// and returns the first off-curve address of seeds plus that one bump byte.
// seeds is one encoded seed group without the bump, so at most 15 seeds.
// output is exactly 33 bytes: the address, then the bump. It returns 0 and
// fills output, 1 when no bump works (output unchanged), 2007 for malformed
// input, or another nonzero status from the derivation. On chain it is one
// sol_try_find_program_address syscall, which charges the same 1,500 CU per
// bump tried as one CreateAddress.
func FindAddress(c Context, seeds, program, output []byte) uint64 {
if len(program) != 32 || len(output) != 33 || len(seeds) > 496 {
return 2007
}
off, ok := validSeedGroup(seeds, 0)
if !ok || off != len(seeds) || seeds[0] > 15 {
return 2007
}
if c.DeriveAddress == nil {
return 2009
}
group := make([]byte, len(seeds)+2)
copy(group, seeds)
group[0]++
group[len(seeds)] = 1
var address [32]byte
for bump := 255; bump >= 1; bump-- {
group[len(seeds)+1] = byte(bump)
code := c.DeriveAddress(group, program, address[:])
if code == 0 {
copy(output, address[:])
output[32] = byte(bump)
return 0
}
if code != 1 {
return code
}
}
return 1
}

// CreateAddress derives an off-curve PDA with an explicit derivation program.
// seeds is one encoded seed group, normally produced by sdk/pda.Seeds.Bytes.
// The output is exactly 32 bytes; the syscall's exact nonzero status propagates.
Expand Down
2 changes: 1 addition & 1 deletion examples/fee-vault/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "f66ccbb30ab2be273f8bcedc30e8f850e2c061d9414f1c081997d1d1b6572c59",
"sdk_sha256": "6e97ac4bb6571bc2d44438fc0a1afe263f678034678e475356660f2301ca503f",
"sdk_version": 2,
"imports": {
"model": "example.org/gosvm/fee-vault/model"
Expand Down
33 changes: 15 additions & 18 deletions examples/find-address/.gosvm/sdk/sdk/pda/pda.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,27 @@ func (s *Seeds) Address(c solana.Context, program [32]byte) ([32]byte, uint64) {
// gives for these seeds under program: each bump from 255 down to 1 (never 0,
// as in Solana) is appended in turn and the first off-curve address is
// returned. The builder is unchanged; add the bump to it for signing. A
// builder with 16 seeds has no room for the bump (ErrSeeds). Each attempt is
// one create-program-address syscall (1,500 CU), so a bump of b costs about
// builder with 16 seeds has no room for the bump (ErrSeeds). The search is
// one solana.FindAddress call (sol_try_find_program_address), which the
// runtime charges 1,500 CU per bump tried, so a bump of b costs about
// (256-b)*1,500 CU; store the bump and use Address when it is known.
func (s *Seeds) FindAddress(c solana.Context, program [32]byte) ([32]byte, byte, uint64) {
var address [32]byte
probe := *s
if probe.AddByte(255) != 0 {
// Room for the one-byte bump seed, as AddByte checks.
if s.count >= 16 || s.used > 526 {
return address, 0, ErrSeeds
}
seeds := probe.Bytes()
last := uint64(len(seeds)) - 1
for bump := uint64(255); bump > 0; bump-- {
seeds[last] = byte(bump)
code := solana.CreateAddress(c, seeds, program[:], address[:])
if code == 0 {
return address, byte(bump), 0
}
// The syscall returns 1 for an on-curve address; other statuses
// (malformed input, a missing native callback) end the search.
if code != 1 {
return address, 0, code
}
var found [33]byte
code := solana.FindAddress(c, s.Bytes(), program[:], found[:])
if code == 1 {
return address, 0, ErrNoAddress
}
// Other statuses (a missing native callback) end the search.
if code != 0 {
return address, 0, code
}
return address, 0, ErrNoAddress
copy(address[:], found[:32])
return address, found[32], 0
}

func (s *Seeds) Matches(c solana.Context, index uint64, program [32]byte) (bool, uint64) {
Expand Down
39 changes: 39 additions & 0 deletions examples/find-address/.gosvm/sdk/solana/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,45 @@ func validSignerSeeds(b []byte) bool {
return off == len(b)
}

// FindAddress is Pubkey::find_program_address: it tries bumps 255 down to 1
// and returns the first off-curve address of seeds plus that one bump byte.
// seeds is one encoded seed group without the bump, so at most 15 seeds.
// output is exactly 33 bytes: the address, then the bump. It returns 0 and
// fills output, 1 when no bump works (output unchanged), 2007 for malformed
// input, or another nonzero status from the derivation. On chain it is one
// sol_try_find_program_address syscall, which charges the same 1,500 CU per
// bump tried as one CreateAddress.
func FindAddress(c Context, seeds, program, output []byte) uint64 {
if len(program) != 32 || len(output) != 33 || len(seeds) > 496 {
return 2007
}
off, ok := validSeedGroup(seeds, 0)
if !ok || off != len(seeds) || seeds[0] > 15 {
return 2007
}
if c.DeriveAddress == nil {
return 2009
}
group := make([]byte, len(seeds)+2)
copy(group, seeds)
group[0]++
group[len(seeds)] = 1
var address [32]byte
for bump := 255; bump >= 1; bump-- {
group[len(seeds)+1] = byte(bump)
code := c.DeriveAddress(group, program, address[:])
if code == 0 {
copy(output, address[:])
output[32] = byte(bump)
return 0
}
if code != 1 {
return code
}
}
return 1
}

// CreateAddress derives an off-curve PDA with an explicit derivation program.
// seeds is one encoded seed group, normally produced by sdk/pda.Seeds.Bytes.
// The output is exactly 32 bytes; the syscall's exact nonzero status propagates.
Expand Down
2 changes: 1 addition & 1 deletion examples/find-address/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ exactly as Solana does.
(`go run ./cmd/fixtures > testdata/svm.json`) with the host's
`fixture.FindProgramAddress` as the oracle. Its authority and one seed are
chosen so the canonical bumps are 252 and 250: the on-chain search skips
on-curve addresses, each attempt costing about 1,570 CU. A valid but
on-curve addresses, each attempt costing 1,500 CU. A valid but
noncanonical bump is refused and the creation rolls back.

```sh
Expand Down
2 changes: 1 addition & 1 deletion examples/find-address/gosvm.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"instruction": "",
"handler": "",
"result": "",
"sdk_sha256": "f66ccbb30ab2be273f8bcedc30e8f850e2c061d9414f1c081997d1d1b6572c59",
"sdk_sha256": "6e97ac4bb6571bc2d44438fc0a1afe263f678034678e475356660f2301ca503f",
"sdk_version": 2,
"imports": {
"model": "example.org/gosvm/find-address/model"
Expand Down
Loading
Loading