diff --git a/HANDOFF.md b/HANDOFF.md index 2f260d98..de9720e6 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,10 +1,28 @@ # goSVM session handoff -Updated October 16, 2026 (America/New_York). +Updated October 19, 2026 (America/New_York). **Current phase:** the user's two pre-core-language hurdles (Fogo/Solana with minimal user awareness; Go+Bend+Rust in one program) are met and merged to `main`; core-language work has started. See the October 9 section below. +**Outgoing CPI bounds raised (October 19, branch `cpi-raise-limits`, local, +unreleased):** for Hyperlane's Sealevel programs (22 metas in IGP `PayForGas` +with quoted accounts, 17 in fee `QuoteFee`), `solana.Invoke`, the C runtime and +`sdk/cpi` now allow 64 account metas and 4 signer groups (seeds unchanged: +16 per group, 32 bytes, 1,024 encoded bytes); [bounds and justification](docs/SDK2.md#cpi-bounds). +`sol_Invoke` is `noinline` with a 2,240-byte frame; `cpi.Metas` keeps 16 records +inline and moves to one 576-byte heap array at the 17th `Add` (a fixed buffer cost ++324 CU per small CPI; a shorter `Add` pushed the full swap's `go_Process` over +4 KiB on v1.57). SDK-2 snapshot change: the escrow, full-swap and tutorial +snapshots and `sdk_sha256` were refreshed, and **existing SDK-2 projects must be +recreated** (Load rejects the old snapshot). Small builder CPIs +9–23 CU, +existing suites +0–21 CU, ELFs +1.6–1.8 KB (the allocator); the full-swap +reference ELFs changed (fogo-v0 `bcd9253e…`, solana-v3 `86f0ed15…`, now +`verify_rebuild.py`'s default reference); 33 corpus C hashes moved. +`gosvm features` does not report the SDK's allocation in `uses.allocation`. +Error-contract change (2006/2007/3002/3003) to announce to Qushta. +[Evidence](results/compiler/2026-10-19-cpi-limits/README.md). + **First-program tutorial and release walkthrough (October 18):** [docs/TUTORIAL.md](docs/TUTORIAL.md) builds a PDA counter ([examples/tutorial](examples/tutorial/README.md), in the CI module loop): scaffold, diff --git a/docs/ERROR_CONTRACT.md b/docs/ERROR_CONTRACT.md index 4aa5fde2..bf8e5941 100644 --- a/docs/ERROR_CONTRACT.md +++ b/docs/ERROR_CONTRACT.md @@ -207,6 +207,8 @@ because fixtures compare them byte for byte: nothing and never shrinks the table. Converting a map to an interface allocates nothing. The full table is in [GO_MAPS.md](GO_MAPS.md#heap-accounting). + SDK 2's `cpi.Metas` allocates 576 bytes at its 17th `Add` and nothing + before; no other SDK helper allocates ([SDK 2](SDK2.md#cpi-bounds)). Package-level variables that are not read-only live in the globals block: a C struct of those variables in `CheckedProgram.Globals` order (each at its Go alignment), rounded up to 8 bytes (`GlobalBytes`). Every invocation @@ -406,8 +408,8 @@ SDK 1 single-account programs use 10–14 for their input checks | 2003 | SHA-256 input or output range out of bounds | | 2004 | SHA-256 result not 32 bytes | | 2005 | Account index out of range | -| 2006 | Malformed CPI account metas | -| 2007 | Malformed signer seeds or PDA input | +| 2006 | Malformed CPI account metas, or more than 64 | +| 2007 | Malformed signer seeds or PDA input, or more than 4 signer groups | | 2008 | CPI instruction data longer than 10,240 bytes | | 2009 | Native callback missing (native tests only) | | 2010 | Resize or original length beyond limits | @@ -424,8 +426,8 @@ SDK 1 single-account programs use 10–14 for their input checks | Code | Meaning | | --- | --- | | 3001 | Seeds: more than 16, or one longer than 32 bytes, or nil where required | -| 3002 | CPI metas: more than 16, or nil | -| 3003 | CPI signers: more than 2 groups or 1,024 bytes | +| 3002 | CPI metas: more than 64, or nil | +| 3003 | CPI signers: more than 4 groups or 1,024 bytes | | 3010 | Token account invalid (owner, executable, 165-byte layout, state, native) | | 3011 | Token account identity | | 3012 | Token mints differ | @@ -648,3 +650,10 @@ Handler results propagate unchanged. Process reaches in package "P", opt in with "trim" in gosvm.json or -trim (docs/GO_PACKAGES.md#trimming)`. No recorded corpus entry's message changed (their dependency packages are not module paths). +- 2026-10-19: CPI bounds raised (contract change; announce to Qushta): 2006 + now allows up to 64 account metas (was 16) and 2007 up to 4 signer groups + (was 2) in `solana.Invoke`; 3002 allows 64 metas and 3003 four groups in + `sdk/cpi`. Inputs within the old bounds give the same codes in the same + order. Seeds keep 16 per group, 32 bytes each and 1,024 encoded bytes. + `cpi.Metas` beyond 16 records allocates 576 bytes (heap accounting above), + so such a CPI can now hit the out-of-memory trap. diff --git a/docs/SDK2.md b/docs/SDK2.md index fa70020c..83c44c73 100644 --- a/docs/SDK2.md +++ b/docs/SDK2.md @@ -37,18 +37,47 @@ 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. These methods use supplied seeds; a bump-search convenience API is not provided. -`cpi.Metas.Add(index, writable, signer)` builds at most 16 account metas. -`cpi.Signers.Add(&seeds)` copies at most two seed groups into a 1,024-byte buffer; -the combined encoding must fit that buffer even if both groups separately fit. +`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 +encoding must fit that buffer even if each group separately fits. `cpi.Invoke` accepts an executable program's context index, metas, at most 10,240 instruction bytes, and optional signer groups. Generic CPI uses explicit account indices and does not itself implement application owner, relationship or alias policy. The SDK-2 account loader accepts up to 32 incoming account descriptors, -including duplicates; SDK 1 remains capped at 16. Outgoing CPI metas and schema-2 -declared accounts retain their separate 16-account bounds. Custom handlers can +including duplicates; SDK 1 remains capped at 16. Schema-2 declared accounts +retain their separate 16-account bound. Custom handlers can accept extra incoming accounts, while generated handlers retain their declared count checks. See the [decoder extension and new pin](ACCOUNT_DECODER32.md). +### CPI bounds + +| Bound | goSVM | Runtime (Agave 3.0) | +| --- | --- | --- | +| Account metas per CPI | 64 (`solana.Invoke` 2006, `Metas.Add` 3002) | 255 (`MAX_ACCOUNTS_PER_INSTRUCTION`) | +| Distinct accounts (account infos) | the incoming accounts, at most 32 | 64, or 128 with `increase_tx_account_lock_limit` | +| Signer groups | 4 (2007, `Signers.Add` 3003) | 16 (`MAX_SIGNERS`) | +| Seeds per group, bytes per seed | 16, 32 (2007; `pda.Seeds` 3001) | 16, 32 (`MAX_SEEDS`, `MAX_SEED_LEN`) | +| Encoded signer groups | 1,024 bytes (2007, 3003) | none | +| Instruction data | 10,240 bytes (2008) | 10,240 | + +Every meta indexes the invocation's own accounts and `Invoke` passes those as +the account infos, so a CPI names at most 32 distinct accounts, within every +feature-set's account-info limit; more than 32 metas repeat accounts. +64 metas cover Hyperlane's Sealevel CPIs (22 for IGP `PayForGas` with quoted +accounts, 17 for fee `QuoteFee`) with room for repeated references, and keep the +C runtime's descriptor arrays (16 bytes per meta and per seed) in one SBF frame: +`sol_Invoke` is never inlined and takes 2,240 bytes of its own frame, so callers' +frames are unchanged. The runtime's 255 would need 4,080 bytes of metas alone. + +`cpi.Metas` keeps its first 16 records (`cpi.InlineMetas`) in the builder. +The 17th `Add` allocates one 576-byte array on the invocation heap, copies the +inline records there and continues in it; that is the only SDK allocation, it +counts toward the heap budget like any other (the out-of-memory trap of +[ERROR_CONTRACT](ERROR_CONTRACT.md#traps)), and a builder that stays within 16 +metas allocates nothing. Copying a `Metas` value after its 17th record shares +the heap array between the copies; pass builders by pointer. + `cpi.SingleSigner` encodes one signer group directly, avoiding the intermediate `pda.Seeds` to `cpi.Signers` copy. It offers `AddBytes` and `AddByte`, with the same maximum of 16 seeds, each at most 32 bytes including the explicit bump. @@ -58,11 +87,11 @@ uses the existing invoke boundary and propagates its statuses unchanged. Nil metas return 3002; a nil signer invokes without PDA signing, matching `Invoke`. A nonnil empty builder encodes one empty group (`[1, 0]`). Keep the borrowed `Bytes` backing value alive and unmodified through invocation. Use the general -builder for two groups. See the [adoption evidence](COMPACT_SIGNER_ADOPTION.md). +builder for two to four groups. See the [adoption evidence](COMPACT_SIGNER_ADOPTION.md). -Builders use caller-owned scalar arrays. `Bytes` returns a borrowed view that -must not escape its backing value. Failed additions leave builders unchanged. -There is no allocation on chain. These bounds are the SDK's experimental +Builders use caller-owned scalar arrays, except `Metas` beyond 16 records (above). +`Bytes` returns a borrowed view that must not escape its backing value. Failed +additions leave builders unchanged. These bounds are the SDK's experimental contract, not a claim to cover all transactions accepted by Solana. `token.Load(context, index, writable)` returns an opaque checked `token.Account`. @@ -217,6 +246,17 @@ output length). These are experimental API codes, separate from schema-2 errors. ## Validation and open work +The [CPI bounds proof](../results/compiler/2026-10-19-cpi-limits/README.md) runs +[examples/cpi-limits](../examples/cpi-limits/program.go) on fogo-v0 and solana-v3: +3, 16, 17, 22, 32 and 64 metas with up to four PDA signer groups, through the +builders and the raw encoding, against a callee that checks its account count, +signers and every repeated-account position; 65 metas and five groups fail +before invoking (3002/3003, 2006/2007). A 3-meta builder CPI costs 9–10 CU more +than before and a 16-meta one 21–23 (the builder's heap pointer and branch); the +raw path is unchanged. Existing token/System CPIs in the escrow, swap and +tutorial suites cost 0–21 CU more per transaction, and every program using +`sdk/cpi` links the allocator (about 1.7 KB of ELF) even when it never spills. + The [checked-token fixture](../examples/checked-token/program.go) and [evidence](../results/compiler/2026-10-05-checked-token-lifecycle-final/README.md) cover multi-seed derivation, two signer groups, real classic Token/System CPIs, diff --git a/examples/cpi-limits/callee.c.txt b/examples/cpi-limits/callee.c.txt new file mode 100644 index 00000000..bb24d19e --- /dev/null +++ b/examples/cpi-limits/callee.c.txt @@ -0,0 +1,47 @@ +/* CPI callee for examples/cpi-limits: instruction data [metas, groups, plain]. + Succeeds only when it received exactly `metas` instruction accounts, its + first `groups` accounts are signers, and the rest cycle through `plain` + distinct accounts: account i is new for i < groups+plain and otherwise a + duplicate of account groups+(i-groups)%plain, which checks every caller + meta record's index. Custom errors: 5000 short data, 5001+count wrong count, + 5300+i account i not a signer, 5600+i account i not the expected + duplicate or new account. No syscalls, so one source builds for v0 and v3. */ +typedef unsigned long long u64; +typedef unsigned char u8; + +static u64 read64(const u8 *p) { + u64 v = 0; + for (u64 j = 0; j < 8; j++) v |= (u64)p[j] << (j * 8); + return v; +} + +u64 entrypoint(u8 *input) { + u8 signer[256], dup[256]; + u64 n = read64(input); + if (n > 255) return 5000; + u8 *p = input + 8; + for (u64 i = 0; i < n; i++) { + dup[i] = p[0]; + if (p[0] != 255) { + signer[i] = signer[p[0]]; + p += 8; + } else { + signer[i] = p[1]; + u64 len = read64(p + 80); + p += 88 + len + 10240; + p = (u8 *)(((u64)p + 7) & ~7ULL); + p += 8; + } + } + u64 len = read64(p); + const u8 *data = p + 8; + if (len < 3) return 5000; + u64 groups = data[1], plain = data[2]; + if (n != data[0]) return 5001 + n; + for (u64 i = 0; i < groups && i < n; i++) if (!signer[i]) return 5300 + i; + for (u64 i = 0; i < n; i++) { + u64 want = i < groups + plain || plain == 0 ? 255 : groups + (i - groups) % plain; + if (dup[i] != want) return 5600 + i; + } + return 0; +} diff --git a/examples/cpi-limits/program.go b/examples/cpi-limits/program.go new file mode 100644 index 00000000..857ff228 --- /dev/null +++ b/examples/cpi-limits/program.go @@ -0,0 +1,112 @@ +// Package cpilimits drives one outgoing CPI with a chosen number of account +// metas and PDA signer groups, through the sdk/cpi builders or the raw +// solana.Invoke encoding. Accounts: 0 the callee program, 1..4 the PDAs of +// seeds ("cpi-limits", group) under this program, then plain accounts that +// the remaining metas cycle through (so counts above the distinct accounts +// repeat them). Instruction: mode, metas, groups, then one bump per group. +// The callee receives [metas, groups, plain] and asserts its account count, +// that its first groups accounts are signers and that the other metas cycle +// through the plain accounts (callee.c.txt). +package cpilimits + +import ( + "gosvm/sdk/cpi" + "gosvm/sdk/pda" + "gosvm/solana" +) + +// Mode selects the encoding path. +const ( + ModeBuilder byte = 0 // cpi.Metas, cpi.Signers, cpi.Invoke + ModeRaw byte = 1 // hand-encoded metas and signer groups, solana.Invoke +) + +// FirstPlain is the first plain account; 1..4 are signer PDAs. +const FirstPlain uint64 = 5 + +// MaxRawMetas and MaxRawGroups bound the raw encoder, one past the limits. +const ( + MaxRawMetas = 65 + MaxRawGroups = 5 +) + +const ErrInput uint64 = 4000 + +// label is the first seed of every signer PDA. +var label = [10]byte{'c', 'p', 'i', '-', 'l', 'i', 'm', 'i', 't', 's'} + +func meta(i, groups, plain uint64) (uint64, bool) { + if i < groups { + return 1 + i, true + } + return FirstPlain + (i-groups)%plain, false +} + +func Process(c solana.Context) uint64 { + ix := solana.Instruction(c) + if len(ix) < 3 || solana.Count(c) <= FirstPlain { + return ErrInput + } + mode, n, groups := ix[0], uint64(ix[1]), uint64(ix[2]) + if uint64(len(ix)) != 3+groups || groups > MaxRawGroups || n > MaxRawMetas || groups > n { + return ErrInput + } + plain := solana.Count(c) - FirstPlain + var data [3]byte + data[0], data[1], data[2] = byte(n), byte(groups), byte(plain) + if mode == ModeRaw { + return raw(c, ix, n, groups, plain, data[:]) + } + var metas cpi.Metas + for i := uint64(0); i < n; i++ { + index, signer := meta(i, groups, plain) + if code := metas.Add(index, false, signer); code != 0 { + return code + } + } + var signers cpi.Signers + for g := uint64(0); g < groups; g++ { + var seeds pda.Seeds + if seeds.AddBytes(label[:]) != 0 || seeds.AddByte(byte(g)) != 0 || seeds.AddByte(ix[3+g]) != 0 { + return pda.ErrSeeds + } + if code := signers.Add(&seeds); code != 0 { + return code + } + } + return cpi.Invoke(c, 0, &metas, data[:], &signers) +} + +// raw encodes up to 65 metas and 5 signer groups directly, so the boundary's +// own limits are reached without the builders' earlier checks. +func raw(c solana.Context, ix []byte, n, groups, plain uint64, data []byte) uint64 { + var metas [MaxRawMetas * 9]byte + for i := uint64(0); i < n; i++ { + index, signer := meta(i, groups, plain) + for j := uint64(0); j < 8; j++ { + metas[i*9+j] = byte(index >> (j * 8)) + } + if signer { + metas[i*9+8] = 2 + } + } + var seeds [1 + MaxRawGroups*16]byte + used := uint64(1) + seeds[0] = byte(groups) + for g := uint64(0); g < groups; g++ { + seeds[used] = 3 + seeds[used+1] = byte(len(label)) + used += 2 + for j := uint64(0); j < uint64(len(label)); j++ { + seeds[used] = label[j] + used++ + } + seeds[used], seeds[used+1], seeds[used+2], seeds[used+3] = 1, byte(g), 1, ix[3+g] + used += 4 + } + signerSeeds := seeds[:used] + if groups == 0 { + signerSeeds = nil + } + return solana.Invoke(c, 0, metas[:n*9], data, signerSeeds) +} diff --git a/examples/escrow/.gosvm/sdk/sdk/cpi/cpi.go b/examples/escrow/.gosvm/sdk/sdk/cpi/cpi.go index be3d9f46..acc66f67 100644 --- a/examples/escrow/.gosvm/sdk/sdk/cpi/cpi.go +++ b/examples/escrow/.gosvm/sdk/sdk/cpi/cpi.go @@ -9,18 +9,26 @@ import ( const ErrMetas uint64 = 3002 const ErrSigners uint64 = 3003 +// MaxMetas and MaxSigners match solana.Invoke: account metas per CPI +// (repeated accounts count) and PDA signer groups per CPI. +const ( + MaxMetas = 64 + MaxSigners = 4 +) + +// InlineMetas records live in the builder itself. The first Add beyond them +// moves the records to one MaxMetas-record array on the invocation heap +// (576 bytes, allocated once), so CPIs of up to 16 metas keep the builder's +// small frame and zeroing cost and never allocate. +const InlineMetas = 16 + type Metas struct { - buffer [144]byte + buffer [InlineMetas * 9]byte count uint64 + wide *[MaxMetas * 9]byte } func (m *Metas) Add(index uint64, writable, signer bool) uint64 { - if m.count >= 16 { - return ErrMetas - } - for j := uint64(0); j < 8; j++ { - m.buffer[m.count*9+j] = byte(index >> (j * 8)) - } flags := byte(0) if writable { flags = flags | 1 @@ -28,12 +36,35 @@ func (m *Metas) Add(index uint64, writable, signer bool) uint64 { if signer { flags = flags | 2 } - m.buffer[m.count*9+8] = flags + if m.count < InlineMetas { + for j := uint64(0); j < 8; j++ { + m.buffer[m.count*9+j] = byte(index >> (j * 8)) + } + m.buffer[m.count*9+8] = flags + } else if m.count < MaxMetas { + if m.wide == nil { + m.wide = new([MaxMetas * 9]byte) + copy(m.wide[:], m.buffer[:]) + } + for j := uint64(0); j < 8; j++ { + m.wide[m.count*9+j] = byte(index >> (j * 8)) + } + m.wide[m.count*9+8] = flags + } else { + return ErrMetas + } m.count++ return 0 } -func (m *Metas) Bytes() []byte { return m.buffer[:m.count*9] } +// Bytes borrows the records: inline, or the heap array after the 17th Add. +// A Metas copied after its records moved shares that array. +func (m *Metas) Bytes() []byte { + if m.count > InlineMetas { + return m.wide[:m.count*9] + } + return m.buffer[:m.count*9] +} type Signers struct { buffer [1024]byte @@ -42,7 +73,7 @@ type Signers struct { } func (s *Signers) Add(seeds *pda.Seeds) uint64 { - if seeds == nil || s.count >= 2 || s.used > 1023 { + if seeds == nil || s.count >= MaxSigners || s.used > 1023 { return ErrSigners } b := seeds.Bytes() @@ -72,7 +103,7 @@ func Invoke(c solana.Context, program uint64, metas *Metas, data []byte, signers // SingleSigner encodes one signer group directly, without a Seeds-to-Signers copy. // It preserves the seed count/length/capacity checks of pda.Seeds. General -// Signers and its two-group/1024-byte checks remain unchanged. +// Signers keeps its four-group/1024-byte checks. // The borrowed encoding must remain alive and unmodified through InvokeSingle. type SingleSigner struct { buffer [530]byte diff --git a/examples/escrow/.gosvm/sdk/solana/api.go b/examples/escrow/.gosvm/sdk/solana/api.go index f094d48f..aa5d7556 100644 --- a/examples/escrow/.gosvm/sdk/solana/api.go +++ b/examples/escrow/.gosvm/sdk/solana/api.go @@ -249,7 +249,8 @@ func Lamports(c Context, i uint64) uint64 { return c.Accounts[i].Lamports } // groups. Applications should use sdk/cpi builders instead of assembling these // encodings. Meta records are index:uint64 LE and flags:byte (writable=1, // signer=2). Signer groups are count:byte, then seed-count:byte followed by each -// length:byte and its bytes. Limits: 16 metas, 2 groups, 16 seeds/group, 32/seed. +// length:byte and its bytes. Limits: 64 metas (duplicates count), 4 groups, +// 16 seeds/group, 32 bytes/seed and 1,024 encoded signer bytes. func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if program >= uint64(len(c.Accounts)) { return 2005 @@ -257,7 +258,7 @@ func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if !c.Accounts[program].Executable { return 2001 } - if len(metas)%9 != 0 || len(metas) > 16*9 { + if len(metas)%9 != 0 || len(metas) > 64*9 { return 2006 } for off := 0; off < len(metas); off += 9 { @@ -305,7 +306,7 @@ func validSignerSeeds(b []byte) bool { if len(b) == 0 { return true } - if len(b) > 1024 || b[0] > 2 { + if len(b) > 1024 || b[0] > 4 { return false } off := 1 diff --git a/examples/escrow/gosvm.json b/examples/escrow/gosvm.json index e996b44c..db2c892e 100644 --- a/examples/escrow/gosvm.json +++ b/examples/escrow/gosvm.json @@ -8,7 +8,7 @@ "instruction": "", "handler": "", "result": "", - "sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd", + "sdk_sha256": "8a11ae1a9129b29ee93207a503287cf6e1149794decf929ff4eb33b24380c235", "sdk_version": 2, "imports": { "model": "example.org/gosvm/escrow/model" diff --git a/examples/full-swap/.gosvm/sdk/sdk/cpi/cpi.go b/examples/full-swap/.gosvm/sdk/sdk/cpi/cpi.go index be3d9f46..acc66f67 100644 --- a/examples/full-swap/.gosvm/sdk/sdk/cpi/cpi.go +++ b/examples/full-swap/.gosvm/sdk/sdk/cpi/cpi.go @@ -9,18 +9,26 @@ import ( const ErrMetas uint64 = 3002 const ErrSigners uint64 = 3003 +// MaxMetas and MaxSigners match solana.Invoke: account metas per CPI +// (repeated accounts count) and PDA signer groups per CPI. +const ( + MaxMetas = 64 + MaxSigners = 4 +) + +// InlineMetas records live in the builder itself. The first Add beyond them +// moves the records to one MaxMetas-record array on the invocation heap +// (576 bytes, allocated once), so CPIs of up to 16 metas keep the builder's +// small frame and zeroing cost and never allocate. +const InlineMetas = 16 + type Metas struct { - buffer [144]byte + buffer [InlineMetas * 9]byte count uint64 + wide *[MaxMetas * 9]byte } func (m *Metas) Add(index uint64, writable, signer bool) uint64 { - if m.count >= 16 { - return ErrMetas - } - for j := uint64(0); j < 8; j++ { - m.buffer[m.count*9+j] = byte(index >> (j * 8)) - } flags := byte(0) if writable { flags = flags | 1 @@ -28,12 +36,35 @@ func (m *Metas) Add(index uint64, writable, signer bool) uint64 { if signer { flags = flags | 2 } - m.buffer[m.count*9+8] = flags + if m.count < InlineMetas { + for j := uint64(0); j < 8; j++ { + m.buffer[m.count*9+j] = byte(index >> (j * 8)) + } + m.buffer[m.count*9+8] = flags + } else if m.count < MaxMetas { + if m.wide == nil { + m.wide = new([MaxMetas * 9]byte) + copy(m.wide[:], m.buffer[:]) + } + for j := uint64(0); j < 8; j++ { + m.wide[m.count*9+j] = byte(index >> (j * 8)) + } + m.wide[m.count*9+8] = flags + } else { + return ErrMetas + } m.count++ return 0 } -func (m *Metas) Bytes() []byte { return m.buffer[:m.count*9] } +// Bytes borrows the records: inline, or the heap array after the 17th Add. +// A Metas copied after its records moved shares that array. +func (m *Metas) Bytes() []byte { + if m.count > InlineMetas { + return m.wide[:m.count*9] + } + return m.buffer[:m.count*9] +} type Signers struct { buffer [1024]byte @@ -42,7 +73,7 @@ type Signers struct { } func (s *Signers) Add(seeds *pda.Seeds) uint64 { - if seeds == nil || s.count >= 2 || s.used > 1023 { + if seeds == nil || s.count >= MaxSigners || s.used > 1023 { return ErrSigners } b := seeds.Bytes() @@ -72,7 +103,7 @@ func Invoke(c solana.Context, program uint64, metas *Metas, data []byte, signers // SingleSigner encodes one signer group directly, without a Seeds-to-Signers copy. // It preserves the seed count/length/capacity checks of pda.Seeds. General -// Signers and its two-group/1024-byte checks remain unchanged. +// Signers keeps its four-group/1024-byte checks. // The borrowed encoding must remain alive and unmodified through InvokeSingle. type SingleSigner struct { buffer [530]byte diff --git a/examples/full-swap/.gosvm/sdk/solana/api.go b/examples/full-swap/.gosvm/sdk/solana/api.go index f094d48f..aa5d7556 100644 --- a/examples/full-swap/.gosvm/sdk/solana/api.go +++ b/examples/full-swap/.gosvm/sdk/solana/api.go @@ -249,7 +249,8 @@ func Lamports(c Context, i uint64) uint64 { return c.Accounts[i].Lamports } // groups. Applications should use sdk/cpi builders instead of assembling these // encodings. Meta records are index:uint64 LE and flags:byte (writable=1, // signer=2). Signer groups are count:byte, then seed-count:byte followed by each -// length:byte and its bytes. Limits: 16 metas, 2 groups, 16 seeds/group, 32/seed. +// length:byte and its bytes. Limits: 64 metas (duplicates count), 4 groups, +// 16 seeds/group, 32 bytes/seed and 1,024 encoded signer bytes. func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if program >= uint64(len(c.Accounts)) { return 2005 @@ -257,7 +258,7 @@ func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if !c.Accounts[program].Executable { return 2001 } - if len(metas)%9 != 0 || len(metas) > 16*9 { + if len(metas)%9 != 0 || len(metas) > 64*9 { return 2006 } for off := 0; off < len(metas); off += 9 { @@ -305,7 +306,7 @@ func validSignerSeeds(b []byte) bool { if len(b) == 0 { return true } - if len(b) > 1024 || b[0] > 2 { + if len(b) > 1024 || b[0] > 4 { return false } off := 1 diff --git a/examples/full-swap/gosvm.json b/examples/full-swap/gosvm.json index 6b1b2911..59aa605a 100644 --- a/examples/full-swap/gosvm.json +++ b/examples/full-swap/gosvm.json @@ -8,7 +8,7 @@ "instruction": "", "handler": "", "result": "", - "sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd", + "sdk_sha256": "8a11ae1a9129b29ee93207a503287cf6e1149794decf929ff4eb33b24380c235", "sdk_version": 2, "imports": { "model": "example.org/gosvm/full-swap/model" diff --git a/examples/tutorial/.gosvm/sdk/sdk/cpi/cpi.go b/examples/tutorial/.gosvm/sdk/sdk/cpi/cpi.go index be3d9f46..acc66f67 100644 --- a/examples/tutorial/.gosvm/sdk/sdk/cpi/cpi.go +++ b/examples/tutorial/.gosvm/sdk/sdk/cpi/cpi.go @@ -9,18 +9,26 @@ import ( const ErrMetas uint64 = 3002 const ErrSigners uint64 = 3003 +// MaxMetas and MaxSigners match solana.Invoke: account metas per CPI +// (repeated accounts count) and PDA signer groups per CPI. +const ( + MaxMetas = 64 + MaxSigners = 4 +) + +// InlineMetas records live in the builder itself. The first Add beyond them +// moves the records to one MaxMetas-record array on the invocation heap +// (576 bytes, allocated once), so CPIs of up to 16 metas keep the builder's +// small frame and zeroing cost and never allocate. +const InlineMetas = 16 + type Metas struct { - buffer [144]byte + buffer [InlineMetas * 9]byte count uint64 + wide *[MaxMetas * 9]byte } func (m *Metas) Add(index uint64, writable, signer bool) uint64 { - if m.count >= 16 { - return ErrMetas - } - for j := uint64(0); j < 8; j++ { - m.buffer[m.count*9+j] = byte(index >> (j * 8)) - } flags := byte(0) if writable { flags = flags | 1 @@ -28,12 +36,35 @@ func (m *Metas) Add(index uint64, writable, signer bool) uint64 { if signer { flags = flags | 2 } - m.buffer[m.count*9+8] = flags + if m.count < InlineMetas { + for j := uint64(0); j < 8; j++ { + m.buffer[m.count*9+j] = byte(index >> (j * 8)) + } + m.buffer[m.count*9+8] = flags + } else if m.count < MaxMetas { + if m.wide == nil { + m.wide = new([MaxMetas * 9]byte) + copy(m.wide[:], m.buffer[:]) + } + for j := uint64(0); j < 8; j++ { + m.wide[m.count*9+j] = byte(index >> (j * 8)) + } + m.wide[m.count*9+8] = flags + } else { + return ErrMetas + } m.count++ return 0 } -func (m *Metas) Bytes() []byte { return m.buffer[:m.count*9] } +// Bytes borrows the records: inline, or the heap array after the 17th Add. +// A Metas copied after its records moved shares that array. +func (m *Metas) Bytes() []byte { + if m.count > InlineMetas { + return m.wide[:m.count*9] + } + return m.buffer[:m.count*9] +} type Signers struct { buffer [1024]byte @@ -42,7 +73,7 @@ type Signers struct { } func (s *Signers) Add(seeds *pda.Seeds) uint64 { - if seeds == nil || s.count >= 2 || s.used > 1023 { + if seeds == nil || s.count >= MaxSigners || s.used > 1023 { return ErrSigners } b := seeds.Bytes() @@ -72,7 +103,7 @@ func Invoke(c solana.Context, program uint64, metas *Metas, data []byte, signers // SingleSigner encodes one signer group directly, without a Seeds-to-Signers copy. // It preserves the seed count/length/capacity checks of pda.Seeds. General -// Signers and its two-group/1024-byte checks remain unchanged. +// Signers keeps its four-group/1024-byte checks. // The borrowed encoding must remain alive and unmodified through InvokeSingle. type SingleSigner struct { buffer [530]byte diff --git a/examples/tutorial/.gosvm/sdk/solana/api.go b/examples/tutorial/.gosvm/sdk/solana/api.go index f094d48f..aa5d7556 100644 --- a/examples/tutorial/.gosvm/sdk/solana/api.go +++ b/examples/tutorial/.gosvm/sdk/solana/api.go @@ -249,7 +249,8 @@ func Lamports(c Context, i uint64) uint64 { return c.Accounts[i].Lamports } // groups. Applications should use sdk/cpi builders instead of assembling these // encodings. Meta records are index:uint64 LE and flags:byte (writable=1, // signer=2). Signer groups are count:byte, then seed-count:byte followed by each -// length:byte and its bytes. Limits: 16 metas, 2 groups, 16 seeds/group, 32/seed. +// length:byte and its bytes. Limits: 64 metas (duplicates count), 4 groups, +// 16 seeds/group, 32 bytes/seed and 1,024 encoded signer bytes. func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if program >= uint64(len(c.Accounts)) { return 2005 @@ -257,7 +258,7 @@ func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if !c.Accounts[program].Executable { return 2001 } - if len(metas)%9 != 0 || len(metas) > 16*9 { + if len(metas)%9 != 0 || len(metas) > 64*9 { return 2006 } for off := 0; off < len(metas); off += 9 { @@ -305,7 +306,7 @@ func validSignerSeeds(b []byte) bool { if len(b) == 0 { return true } - if len(b) > 1024 || b[0] > 2 { + if len(b) > 1024 || b[0] > 4 { return false } off := 1 diff --git a/examples/tutorial/gosvm.json b/examples/tutorial/gosvm.json index 2ed339d0..296a0416 100644 --- a/examples/tutorial/gosvm.json +++ b/examples/tutorial/gosvm.json @@ -12,7 +12,7 @@ "instruction": "", "handler": "", "result": "", - "sdk_sha256": "c16d2232c17339649c0358ccf3919c20ec1dd1ac2972ec9816480af2d525dadd", + "sdk_sha256": "8a11ae1a9129b29ee93207a503287cf6e1149794decf929ff4eb33b24380c235", "sdk_version": 2, "imports": { "model": "example.com/counter/model" diff --git a/internal/compiler/check_corpus_test.go b/internal/compiler/check_corpus_test.go index 122fc8ef..218ca7af 100644 --- a/internal/compiler/check_corpus_test.go +++ b/internal/compiler/check_corpus_test.go @@ -250,27 +250,27 @@ var corpusChanges = map[int]corpusEntry{ 8: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 10: {CSHA256: "d0af7c7bf21c2bc15e6dfebe12bbb259bad502ab5678ea74668aeb0525631a34"}, // program.go 11: {CSHA256: "513db2a5c364047e3a630a5f2e38599aa5e1eb41c8b4658c3c4067b139aeff66"}, // entry.go - 20: {CSHA256: "328f4e2ee7ba46263674d9f6b7241eb79623a4497207c823f3928dde3336a6f1"}, // model.go - 22: {CSHA256: "dd4b8fe6acdc35feb26a086c9e48389a06d93e779aa48b64eb71d1b92691ebe6"}, // pda.go + 20: {CSHA256: "9eea3c26d783a4a139269f4ee7ce08370c2c4c348e7215292f513a2ce1538d03"}, // model.go; then the CPI bounds (below) + 22: {CSHA256: "8b36bc705cf986395c265e047fb7aa133b7e2561d4e259172930b6af11ee7358"}, // pda.go; then the CPI bounds (below) 40: {CSHA256: "8ea5fabbd6de885686956be6f6e294059b5115153b3866006e3d8a1deb7373ca"}, // program.go - 43: {CSHA256: "328f4e2ee7ba46263674d9f6b7241eb79623a4497207c823f3928dde3336a6f1"}, // model.go + 43: {CSHA256: "9eea3c26d783a4a139269f4ee7ce08370c2c4c348e7215292f513a2ce1538d03"}, // model.go; then the CPI bounds (below) 48: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 51: {CSHA256: "513db2a5c364047e3a630a5f2e38599aa5e1eb41c8b4658c3c4067b139aeff66"}, // types.go 57: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 58: {CSHA256: "7e0975a8e457446af4c53308487b583a1fd28d90c586064e235a2c578bb9c6e7"}, // model.go 59: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 68: {CSHA256: "8da0bb74f191cf0df3dac6d2b84576707539f28f444ad62c894f22e5f4ac15e8"}, // model.go + 68: {CSHA256: "b243ca6e867114fc883d29532f1d76a85183bb8e349e676715807bd4e3373b6d"}, // model.go; then the CPI bounds (below) 77: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 83: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 85: {CSHA256: "4b3199a3e995310ad3c61320415f382481451b2510de256382b9aad8fff44f52"}, // model.go - 86: {CSHA256: "8da0bb74f191cf0df3dac6d2b84576707539f28f444ad62c894f22e5f4ac15e8"}, // model.go + 85: {CSHA256: "a033ec1c6d2eabaac330c3c60ee55775aa603965f81066ca83904ee185e83438"}, // model.go; then the CPI bounds (below) + 86: {CSHA256: "b243ca6e867114fc883d29532f1d76a85183bb8e349e676715807bd4e3373b6d"}, // model.go; then the CPI bounds (below) 92: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 93: {CSHA256: "d0af7c7bf21c2bc15e6dfebe12bbb259bad502ab5678ea74668aeb0525631a34"}, // test.go 99: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 105: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 108: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 112: {CSHA256: "fdce1b88a9e6af0dd2036f6bf131b190aaa0809d58b9d81517a2ca69391e43f2"}, // model.go - 114: {CSHA256: "57baa962535ac47f6fcde53076bbb9138f6cfc467cc7e9d729ce4f903421712f"}, // pda.go + 112: {CSHA256: "e4a852c6db22eefec5f520cbcd4b7f609ab4b9cbf718c1000a155366fd8d7984"}, // model.go; then the CPI bounds (below) + 114: {CSHA256: "d508df22ee50a02bdf37a08cf366b6c97ba3d310baca4ce84795dd6a75957a60"}, // pda.go; then the CPI bounds (below) 115: {CSHA256: "ff9d74d9cd831d0c92b087ea98d60ea3d16b969ddd582013014146d070930048"}, // model.go 117: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 118: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go @@ -280,30 +280,52 @@ var corpusChanges = map[int]corpusEntry{ 143: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 147: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 160: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 167: {CSHA256: "fdce1b88a9e6af0dd2036f6bf131b190aaa0809d58b9d81517a2ca69391e43f2"}, // model.go + 167: {CSHA256: "e4a852c6db22eefec5f520cbcd4b7f609ab4b9cbf718c1000a155366fd8d7984"}, // model.go; then the CPI bounds (below) 169: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 172: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 176: {CSHA256: "c4320970229098e1a01bca4f4d73f17242472cc2d8ca081d13fab5479e5d078b"}, // model.go + 176: {CSHA256: "4d5e4c37f3960bd864eec5d57db4feec57401f779a0922b8d8ef644504964dc9"}, // model.go; then the CPI bounds (below) 181: {CSHA256: "f3ec7d095818e1b1d8339cad5a0a05a40a47c8d14991736b41c7eb6094046e77"}, // model.go 192: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 206: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 207: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 208: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 210: {CSHA256: "2157f52add09a2752cf5657c42aad883a793183c0df593203edee727232c7933"}, // pda.go + 210: {CSHA256: "9972da2424b26332db0ffefc83d0c1206dd02f978c7fa2e8802cf1acf0b3d15a"}, // pda.go; then the CPI bounds (below) 211: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 217: {CSHA256: "81cbd346e56642a8063bd09788313c7714ee4e12bf95a201fdab93b8bed98ec1"}, // model.go + 217: {CSHA256: "53dd42168007cd967b671f8d2809218c04114f41e6d2946682d78c434a5e0adb"}, // model.go; then the CPI bounds (below) 219: {CSHA256: "03a66c44f073b48586edb8f45ff3f74cf3d91eceee4f3cf3480ed5e3e17e0950"}, // arrays.go 225: {CSHA256: "d0af7c7bf21c2bc15e6dfebe12bbb259bad502ab5678ea74668aeb0525631a34"}, // arrays.go 227: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 242: {CSHA256: "54eb5126883501afd5be891aeb609c4f5d55f5ffdd9a540163a889b16c6bac8f"}, // model.go + 242: {CSHA256: "2951b8daf4fbbcd53df58a0715fd040b930ee9ecfae912ef3a3c987fc16b2653"}, // model.go; then the CPI bounds (below) 247: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 270: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 278: {CSHA256: "d0af7c7bf21c2bc15e6dfebe12bbb259bad502ab5678ea74668aeb0525631a34"}, // arrays.go - 281: {CSHA256: "b50201ffc29dbc8dfa9be0bc6a07f2d3eb16488b5d40009303b6b5bddfdad53b"}, // pda.go + 281: {CSHA256: "16142e17f43688e613ff873a0dfde14930f11661560529abb6c7a104d25ac748"}, // pda.go; then the CPI bounds (below) 283: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go 300: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go - 302: {CSHA256: "25b08138fe9dd864e7dbeb13f53c1167b5697621e9311b66363f2bea5d3c65ba"}, // model.go + 302: {CSHA256: "20a027a31cf7ecee4e78344692d5e2a2ec2c30a6972827bf9a1ba00bb900f8d0"}, // model.go; then the CPI bounds (below) 309: {CSHA256: "01f287d402774053e6ca44fdcaa3157cd7f6c348df854f5098092cd87ab06724"}, // model.go + // CPI bounds (docs/SDK2.md#cpi-bounds): no verdict changed. The SDK-2 C + // runtime's sol_Invoke now allows 64 metas and 4 signer groups and is + // never inlined, which changes the C of every recorded program that + // includes it: the 15 entries above marked "the CPI bounds" and these. + 0: {CSHA256: "1727cec46995666a495a458c870dc09a39999d3568eb2c56ec1ae3eb32268990"}, + 69: {CSHA256: "20ac5f3f22f5ced2dc9f15ba7a044960acf5dcc82512344172bb988820c61ab2"}, + 75: {CSHA256: "40ca71a5341383b89015c6eb3abda6be0168266436bf380a6d43dc7eb2b911ac"}, + 95: {CSHA256: "483aeb8d977f92fde8df69df3d7e6fc899bb9f33c7f5ded03066d47a3f4f80bc"}, + 97: {CSHA256: "da6e475c71a3bc3e043491d437671a37fc36a2648c537c9972c89d901fbb6809"}, + 109: {CSHA256: "3c8db21e9def028d47a45fba621349190ff89bab48467e121decd649390d85f7"}, + 129: {CSHA256: "769cf777dbd533eac1091ef4764276fd7e04a3c3b5707f0132090d54845b86fc"}, + 144: {CSHA256: "50e50e516938ed118c6f2c8e06c1ed64bf0219d9cebaef75f94630e67419ef37"}, + 152: {CSHA256: "4363f365bfe841379440ad9cb37fcecac61985311379d87c25c0b41075f8c23b"}, + 158: {CSHA256: "b7a06bf33a1bf33cf8d147223c92257bb4e0ac9ea911300a87df452ef6fe8b09"}, + 170: {CSHA256: "8f88245bddaccf43cf3a3915e643aa04fa9fddf0e71c95f7b5edbc5b7b813baf"}, + 174: {CSHA256: "8ab3c4f648c4dd65996dd8e7b58e4ed2498d8e60857a7996f54724acf3966b06"}, + 243: {CSHA256: "d6155fcce27285820eadc2b9b0fcee8738fee33d6e597a355581ca2c8e2dfb90"}, + 252: {CSHA256: "b554c9adaec403339df872c13c09658fb3c39a2320294d4ec66f9ddd996b4b1a"}, + 264: {CSHA256: "6fbb02e34c6b86c049d80e250e9aedea215ab1638d34a4cade1f6b70b856992c"}, + 279: {CSHA256: "a490ff6230552064bb25a323f268729ea71e6d59fcf0b454724b2ceaa47f75d3"}, + 294: {CSHA256: "07cb8f81bccffd7a03986d9b11e2cfd010d8033526cb02ad6228cba10e2c8cff"}, + 306: {CSHA256: "593b0f353b68b5b19a2362135a4b2686534656e44984331b2f3a2af7156dc9a1"}, } const ( diff --git a/internal/compiler/cpi.c.txt b/internal/compiler/cpi.c.txt index 98d15f9b..27590e0f 100644 --- a/internal/compiler/cpi.c.txt +++ b/internal/compiler/cpi.c.txt @@ -102,23 +102,29 @@ static u64 sol_CreateAddress(Context *c,slice b,slice program,slice output) { return 0; } -static u64 sol_Invoke(Context *c,u64 program,slice metas,slice data,slice encoded) { +// Outgoing CPI bounds (solana/api.go Invoke, docs/SDK2.md#cpi-bounds): 64 +// account metas, 4 signer groups of at most 16 seeds of at most 32 bytes, in +// at most 1,024 encoded bytes. The descriptor arrays take 2,112 bytes, so +// sol_Invoke keeps its own SBF frame rather than growing its caller's. +#define GOSVM_CPI_METAS 64 +#define GOSVM_CPI_GROUPS 4 +static __attribute__((noinline)) u64 sol_Invoke(Context *c,u64 program,slice metas,slice data,slice encoded) { if(program>=c->count) return 2005; AccountInfo *p=&c->accounts[program];if(!p->executable) return 2001; // Slice lengths are nonnegative. Use unsigned remainder: legacy SBF has no // signed-remainder instruction and otherwise emits an unrelated __moddi3. - if((u64)metas.len>16*9 || (u64)metas.len%9) return 2006; - AccountMeta accounts[16];u64 n=(u64)metas.len/9; + if((u64)metas.len>GOSVM_CPI_METAS*9 || (u64)metas.len%9) return 2006; + AccountMeta accounts[GOSVM_CPI_METAS];u64 n=(u64)metas.len/9; for(u64 j=0;j=c->count || flags>3) return 2006; accounts[j]=(AccountMeta){c->accounts[index].key,(flags&1)!=0,(flags&2)!=0}; } if(data.len>10240) return 2008; - Seed parts[32];Seeds groups[2];u64 group_count=0,offset=0; + Seed parts[GOSVM_CPI_GROUPS*16];Seeds groups[GOSVM_CPI_GROUPS];u64 group_count=0,offset=0; if(encoded.len) { if(encoded.len>1024) return 2007; - group_count=encoded.ptr[offset++];if(group_count>2) return 2007; + group_count=encoded.ptr[offset++];if(group_count>GOSVM_CPI_GROUPS) return 2007; for(u64 j=0;j keeps the ELFs, fixtures, reports and per-case CU. +func TestCPILimitsSBF(t *testing.T) { + if testing.Short() { + t.Skip("SBF runs are skipped in -short mode") + } + llvm := modernLLVM() + if llvm == "" { + t.Skip("pinned platform-tools v1.57 backend required") + } + keep := os.Getenv("GOSVM_CPI_RESULTS") + for _, profile := range []string{target.Fogo, target.Solana} { + t.Run(profile, func(t *testing.T) { + runner, err := testvm.RunnerFor("", profile) + if err != nil { + t.Skipf("runner unavailable: %v", err) + } + dir := t.TempDir() + if keep != "" { + dir = filepath.Join(keep, profile) + if err = os.MkdirAll(dir, 0755); err != nil { + t.Fatal(err) + } + } + elf := filepath.Join(dir, "caller.so") + // The repository has no SDK snapshot; select SDK 2 (32 incoming + // accounts) as an SDK-2 project's snapshot would. + program, err := readProgramWith("../../examples/cpi-limits", BuildOptions{}) + if err != nil { + t.Fatal(err) + } + program.SDK = 2 + if err = buildProgram(program, elf, llvm, profile); err != nil { + t.Fatal(err) + } + callee := buildCPICallee(t, llvm, profile, dir) + cases := cpiLimitCases() + fixtures, err := cpiLimitFixtures(cases, fmt.Sprintf("%x", sha256.Sum256(mustRead(t, callee)))) + if err != nil { + t.Fatal(err) + } + fixturePath := filepath.Join(dir, "fixtures.json") + if err = os.WriteFile(fixturePath, fixtures, 0644); err != nil { + t.Fatal(err) + } + p, err := target.Resolve(profile) + if err != nil { + t.Fatal(err) + } + report := filepath.Join(dir, "svm-results.json") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + var log strings.Builder + if err = sbftest.RunGeneral(ctx, elf, fixturePath, report, &log, sbftest.FastOptions{Runner: runner, Profile: p.Name, PlatformTools: p.Tools}); err != nil { + t.Fatalf("%v\n%s", err, log.String()) + } + var parsed struct { + Cases []sbftest.FixtureCaseResult `json:"cases"` + } + if err = json.Unmarshal(mustRead(t, report), &parsed); err != nil { + t.Fatal(err) + } + if len(parsed.Cases) != len(cases)+2 { + t.Fatalf("ran %d of %d cases", len(parsed.Cases), len(cases)+2) + } + summary := map[string]any{"profile": profile, "caller_elf_sha256": fmt.Sprintf("%x", sha256.Sum256(mustRead(t, elf)))} + cu := map[string]uint64{} + for _, c := range parsed.Cases { + cu[c.Name] = c.Steps[0].CU + } + summary["cu"] = cu + encoded, _ := json.MarshalIndent(summary, "", " ") + t.Logf("%s", encoded) + if keep != "" { + if err = os.WriteFile(filepath.Join(dir, "summary.json"), append(encoded, '\n'), 0644); err != nil { + t.Fatal(err) + } + } + }) + } +} diff --git a/results/compiler/2026-10-19-cpi-limits/README.md b/results/compiler/2026-10-19-cpi-limits/README.md new file mode 100644 index 00000000..225162d4 --- /dev/null +++ b/results/compiler/2026-10-19-cpi-limits/README.md @@ -0,0 +1,86 @@ +# Outgoing CPI bounds: 64 metas, 4 signer groups + +October 19, 2026, macOS arm64, platform-tools v1.57 (fogo-v0, solana-v3), runners +0.5.1 and 0.6.0. Local LiteSVM runs only; no network validation. + +`solana.Invoke`, the SDK-2 C runtime (`sol_Invoke`) and `sdk/cpi` now allow 64 +account metas (was 16) and 4 PDA signer groups (was 2). Seeds stay at 16 per +group, 32 bytes each, 1,024 encoded bytes. Bounds and their justification are in +[SDK 2](../../../docs/SDK2.md#cpi-bounds); the contract change is in the +[error contract](../../../docs/ERROR_CONTRACT.md) changelog. + +## Design + +- `sol_Invoke` has 64-entry meta and 4x16-entry seed descriptor arrays and is + `noinline`: its own frame grows 960 → 2,240 bytes; callers' frames do not. +- `cpi.Metas` keeps 16 records inline and moves them to one 576-byte heap array + at the 17th `Add`. A fixed 576-byte buffer was measured first and rejected: + zeroing it cost +324 CU on every 3-meta CPI. A shorter `Add` body (slicing one + record) was also rejected: it changed LLVM's inlining so that the full swap's + `go_Process` frame grew 3,904 → 4,480 bytes, a build error on v1.57. The + committed `Add` keeps the original per-byte indexing. +- `cpi.Signers` keeps its 1,024-byte buffer; only the group count changed. + +## Evidence + +`TestCPILimitsSBF` builds [examples/cpi-limits](../../../examples/cpi-limits/program.go) +for both profiles (SDK-2 decoder, 29 incoming accounts) and a C callee +([callee.c.txt](../../../examples/cpi-limits/callee.c.txt)) that fails unless it +receives exactly the requested number of accounts, its first `groups` accounts +are signers and every later account is the expected new account or duplicate +(so each meta record's index, including the heap-spilled ones, is checked +on chain). 19 CPI cases plus two direct callee checks pass on both profiles: +3/16/17/22/32/64 metas with 0–4 groups through the builders and the raw +encoding; 65 metas → 3002 (builder) / 2006 (raw); 5 groups → 3003 / 2007. + +Native: `sdk/cpi` tests cover 64 records (inline/heap switch at 17), the 65th, +four groups, the fifth, and the 64-meta/4-group callback. The native-Go/C +boundary differential now draws metas up to 66 records and one to five random +signer groups, plus 16/17/22/63/64/65 metas with four full 16-seed groups. + +[cu.json](cu.json) has every case. Before = `origin/main` (79cf25c) with the same +harness, old-limit cases only; totals include the 29-account entry decode and +the callee. + +| Case | fogo-v0 before | after | solana-v3 before | after | +| --- | ---: | ---: | ---: | ---: | +| builder, 3 metas | 5,136 | 5,146 | 5,091 | 5,101 | +| builder, 3 metas, 1 group | 5,392 | 5,401 | 5,347 | 5,356 | +| builder, 16 metas | 7,073 | 7,096 | 7,030 | 7,053 | +| raw, 3 metas | 4,414 | 4,414 | 4,369 | 4,369 | +| raw, 16 metas, 2 groups | 5,524 | 5,524 | 5,481 | 5,481 | +| builder, 17 metas | 3002 | 7,308 | 3002 | 7,266 | +| builder, 22 metas | 3002 | 8,068 | 3002 | 8,031 | +| builder, 22 metas, 4 groups | 3002 | 9,049 | 3002 | 9,012 | +| raw, 22 metas, 4 groups | 2006 | 6,158 | 2006 | 6,115 | +| builder, 64 metas, 4 groups | 3002 | 15,577 | 3002 | 15,588 | + +The 17th meta costs about 210 CU (the allocation, the 144-byte copy and one more +account); later metas about 150 each, mostly runtime and callee. + +Existing suites (`gosvm test --svm`): escrow 42 steps (legacy profile), full swap +15 (legacy), tutorial 18 (both profiles) pass unchanged; CU +0 to +14, +21 and ++7. ELFs grow 1,640–1,848 bytes because `sdk/cpi` now reaches the allocator. + +Static frames (v1.57; escrow and full swap also v1.51): full swap `go_Process` 3,904 and escrow/tutorial +`Metas` holders 1,408/1,920, all unchanged; `sol_Invoke` 2,240. + +[metrics.json](metrics.json) is `scripts/verify_rebuild.py` (now the default +reference): fresh rebuilds are identical; the full swap changed to +`bcd9253e…` (fogo-v0) and `86f0ed15…` (solana-v3), clock2 is unchanged. +The check corpus lists the 33 C hashes the runtime change moved. + +## Pre-existing, not caused here + +- The checked-token fixture, staged as an SDK-2 project, already fails to build + on `main` under v1.57 (`entrypoint` 4,544 bytes). The full swap's `go_Process` + is 192 bytes under the limit, so small SDK changes can flip it. +- With host Go 1.23.2, `TestStdlib*` and `TestFullSwapFacts` (`/tmp` vs + `/private/tmp` paths) fail on `main` too. + +## Reproduce + +```sh +GOSVM_CPI_RESULTS= make test # or: go test -run 'TestCPILimitsSBF|TestSDK2InvokeBoundaryDifferential' ./internal/compiler +python3 scripts/verify_rebuild.py --output +``` diff --git a/results/compiler/2026-10-19-cpi-limits/cu.json b/results/compiler/2026-10-19-cpi-limits/cu.json new file mode 100644 index 00000000..a4f2b7f8 --- /dev/null +++ b/results/compiler/2026-10-19-cpi-limits/cu.json @@ -0,0 +1,262 @@ +{ + "baseline": "origin/main 79cf25c with this branch's example, callee and TestCPILimitsSBF (old-limit cases only)", + "examples_test_svm": { + "escrow": { + "changed": 11, + "changed_steps": { + "legacy-v1.51-v3 atomic-close-bad-open-rollback step 1": [ + 23790, + 23797 + ], + "legacy-v1.51-v3 atomic-open-bad-claim-rollback step 1": [ + 27963, + 27977 + ], + "legacy-v1.51-v3 current-rent-lifecycle-0 step 1": [ + 20837, + 20851 + ], + "legacy-v1.51-v3 current-rent-lifecycle-1 step 1": [ + 20356, + 20370 + ], + "legacy-v1.51-v3 deposit-cpi-failure-rollback step 1": [ + 19024, + 19038 + ], + "legacy-v1.51-v3 failed-claim-then-success step 1": [ + 20849, + 20863 + ], + "legacy-v1.51-v3 open-cancel-reuse step 1": [ + 20849, + 20863 + ], + "legacy-v1.51-v3 open-cancel-reuse step 3": [ + 20849, + 20863 + ], + "legacy-v1.51-v3 open-claim-reuse step 1": [ + 20849, + 20863 + ], + "legacy-v1.51-v3 open-claim-reuse step 3": [ + 20849, + 20863 + ], + "legacy-v1.51-v3 zero-deposit-rollback step 1": [ + 17034, + 17041 + ] + }, + "cu_delta_max": 14, + "cu_delta_min": 0, + "elf_bytes_after": [ + 27232 + ], + "elf_bytes_before": [ + 25384 + ], + "steps": 42 + }, + "full-swap": { + "changed": 6, + "changed_steps": { + "legacy-v1.51-v3 input-frozen step 1": [ + 32762, + 32774 + ], + "legacy-v1.51-v3 output-frozen step 1": [ + 43980, + 44001 + ], + "legacy-v1.51-v3 stateful-swaps-and-atomic-rollback step 1": [ + 46631, + 46651 + ], + "legacy-v1.51-v3 stateful-swaps-and-atomic-rollback step 2": [ + 46626, + 46646 + ], + "legacy-v1.51-v3 stateful-swaps-and-atomic-rollback step 3": [ + 71885, + 71905 + ], + "legacy-v1.51-v3 wide-swap step 1": [ + 46631, + 46651 + ] + }, + "cu_delta_max": 21, + "cu_delta_min": 0, + "elf_bytes_after": [ + 70112 + ], + "elf_bytes_before": [ + 68472 + ], + "steps": 15 + }, + "tutorial": { + "changed": 8, + "changed_steps": { + "fogo-v0 create-and-increment step 1": [ + 14844, + 14851 + ], + "fogo-v0 failed-create-rolls-back step 1": [ + 14000, + 14007 + ], + "fogo-v0 increment-past-limit step 1": [ + 14844, + 14851 + ], + "fogo-v0 only-the-owner-increments step 1": [ + 14844, + 14851 + ], + "solana-v3 create-and-increment step 1": [ + 14790, + 14797 + ], + "solana-v3 failed-create-rolls-back step 1": [ + 13946, + 13953 + ], + "solana-v3 increment-past-limit step 1": [ + 14790, + 14797 + ], + "solana-v3 only-the-owner-increments step 1": [ + 14790, + 14797 + ] + }, + "cu_delta_max": 7, + "cu_delta_min": 0, + "elf_bytes_after": [ + 17640, + 16464 + ], + "elf_bytes_before": [ + 16000, + 14824 + ], + "steps": 18 + } + }, + "network_validation": false, + "profiles": { + "fogo-v0": { + "caller_elf_sha256": { + "after": "6dc542787124799f272ee4bb505ff7829d37b999b4eaf3674388488b789b9bff", + "before": "19f74dad8cc5f7ee006c3eaa2fee01c2638ac27daf6eda1b6444a70b2a32f45a" + }, + "cu_after": { + "builder-16-metas": 7096, + "builder-16-metas-2-groups": 7593, + "builder-17-metas": 7308, + "builder-17-metas-2-groups": 7805, + "builder-22-metas": 8068, + "builder-22-metas-4-groups": 9049, + "builder-3-metas": 5146, + "builder-3-metas-1-group": 5401, + "builder-32-metas-4-groups": 10585, + "builder-5-groups": 4788, + "builder-64-metas": 14612, + "builder-64-metas-4-groups": 15577, + "builder-65-metas": 8906, + "callee-rejects-wrong-count": 73, + "callee-rejects-wrong-duplicate": 98, + "raw-16-metas-2-groups": 5524, + "raw-22-metas-4-groups": 6158, + "raw-3-metas": 4414, + "raw-5-groups": 3452, + "raw-64-metas-4-groups": 9032, + "raw-65-metas": 3687 + }, + "cu_before": { + "builder-16-metas": 7073, + "builder-16-metas-2-groups": 7572, + "builder-17-metas": 4120, + "builder-3-groups": 4470, + "builder-3-metas": 5136, + "builder-3-metas-1-group": 5392, + "callee-rejects-wrong-count": 73, + "callee-rejects-wrong-duplicate": 98, + "raw-16-metas-2-groups": 5524, + "raw-17-metas": 3159, + "raw-3-groups": 3387, + "raw-3-metas": 4414 + } + }, + "solana-v3": { + "caller_elf_sha256": { + "after": "dc12871ab8f0748d91fea0822305a300309c8a1419095267443d01c444199473", + "before": "4f80fb33de5488760e29660d0fc0d32f8eb17bf7f494fc4c90f3768aa6b6a1d5" + }, + "cu_after": { + "builder-16-metas": 7053, + "builder-16-metas-2-groups": 7550, + "builder-17-metas": 7266, + "builder-17-metas-2-groups": 7763, + "builder-22-metas": 8031, + "builder-22-metas-4-groups": 9012, + "builder-3-metas": 5101, + "builder-3-metas-1-group": 5356, + "builder-32-metas-4-groups": 10560, + "builder-5-groups": 4788, + "builder-64-metas": 14623, + "builder-64-metas-4-groups": 15588, + "builder-65-metas": 8954, + "callee-rejects-wrong-count": 73, + "callee-rejects-wrong-duplicate": 98, + "raw-16-metas-2-groups": 5481, + "raw-22-metas-4-groups": 6115, + "raw-3-metas": 4369, + "raw-5-groups": 3452, + "raw-64-metas-4-groups": 8995, + "raw-65-metas": 3687 + }, + "cu_before": { + "builder-16-metas": 7030, + "builder-16-metas-2-groups": 7529, + "builder-17-metas": 4120, + "builder-3-groups": 4470, + "builder-3-metas": 5091, + "builder-3-metas-1-group": 5347, + "callee-rejects-wrong-count": 73, + "callee-rejects-wrong-duplicate": 98, + "raw-16-metas-2-groups": 5481, + "raw-17-metas": 3159, + "raw-3-groups": 3387, + "raw-3-metas": 4369 + } + } + }, + "schema": 1, + "static_frames_bytes": { + "cpi-limits entrypoint": { + "after": 3200, + "before": 3200 + }, + "escrow largest Metas holder": { + "after": 1408, + "before": 1408 + }, + "full-swap go_Process (fogo-v0, solana-v3, legacy)": { + "after": 3904, + "before": 3904 + }, + "sol_Invoke": { + "after": 2240, + "before": 960 + }, + "tutorial largest Metas holder": { + "after": 1920, + "before": 1920 + } + }, + "workload": "examples/cpi-limits (29 incoming accounts) calling examples/cpi-limits/callee.c.txt" +} \ No newline at end of file diff --git a/results/compiler/2026-10-19-cpi-limits/metrics.json b/results/compiler/2026-10-19-cpi-limits/metrics.json new file mode 100644 index 00000000..1a79baa5 --- /dev/null +++ b/results/compiler/2026-10-19-cpi-limits/metrics.json @@ -0,0 +1,128 @@ +{ + "schema": 1, + "network_validation": false, + "host": "macOS-27.0.1-arm64-arm-64bit-Mach-O", + "machine": "arm64", + "cli": "build/gosvm", + "cli_sha256": "136516a455b6fe779c4e401ca741db61e9e705851f0bd65fb510a340d0bf1899", + "build_flags": [ + "-no-cache" + ], + "go_version": "go version go1.23.2 darwin/arm64", + "workloads": { + "full-swap": "examples/full-swap", + "clock2": "examples/clock2" + }, + "skipped": { + "go-bend-rust": "needs pinned Bend producer manifests and the full Rust producer tree; not part of this offline rebuild check" + }, + "results": [ + { + "target": "fogo-v0", + "workload": "full-swap", + "kind": "project", + "identical": true, + "no_embedded_host_strings": true, + "elf_sha256": "bcd9253e0ca6d1f1099fc19740f5d31facecd895243b510f97751ecf35ed16bb", + "sides": [ + { + "side": "a", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-a-j_ap53gt/work/a/full-swap", + "elf_sha256": "bcd9253e0ca6d1f1099fc19740f5d31facecd895243b510f97751ecf35ed16bb", + "size": 70600, + "embedded_host_strings": [] + }, + { + "side": "bb-longer-path", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-bb-longer-path-x1cvglm7/work/bb-longer-path/full-swap", + "elf_sha256": "bcd9253e0ca6d1f1099fc19740f5d31facecd895243b510f97751ecf35ed16bb", + "size": 70600, + "embedded_host_strings": [] + } + ], + "matches_reference": false + }, + { + "target": "fogo-v0", + "workload": "clock2", + "kind": "low-level", + "identical": true, + "no_embedded_host_strings": true, + "elf_sha256": "0f768370b8f8dd080eacee738fbf87c95fcda6ddf8532246b3f36ea8ad9c3804", + "sides": [ + { + "side": "a", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-a-qa0gx8c2/work/a/clock2", + "elf_sha256": "0f768370b8f8dd080eacee738fbf87c95fcda6ddf8532246b3f36ea8ad9c3804", + "size": 5304, + "embedded_host_strings": [] + }, + { + "side": "bb-longer-path", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-bb-longer-path-26b71sbr/work/bb-longer-path/clock2", + "elf_sha256": "0f768370b8f8dd080eacee738fbf87c95fcda6ddf8532246b3f36ea8ad9c3804", + "size": 5304, + "embedded_host_strings": [] + } + ], + "matches_reference": true + }, + { + "target": "solana-v3", + "workload": "full-swap", + "kind": "project", + "identical": true, + "no_embedded_host_strings": true, + "elf_sha256": "86f0ed1548c4257602370c7bebb3ef6af9399eaf78ca50d2cc45e55d81669b64", + "sides": [ + { + "side": "a", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-a-96o73pi4/work/a/full-swap", + "elf_sha256": "86f0ed1548c4257602370c7bebb3ef6af9399eaf78ca50d2cc45e55d81669b64", + "size": 70760, + "embedded_host_strings": [] + }, + { + "side": "bb-longer-path", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-bb-longer-path-etfuvhvk/work/bb-longer-path/full-swap", + "elf_sha256": "86f0ed1548c4257602370c7bebb3ef6af9399eaf78ca50d2cc45e55d81669b64", + "size": 70760, + "embedded_host_strings": [] + } + ], + "matches_reference": false + }, + { + "target": "solana-v3", + "workload": "clock2", + "kind": "low-level", + "identical": true, + "no_embedded_host_strings": true, + "elf_sha256": "54f34b65e934efa720f9755179e03e8b0edc6abeb8039f2788a03dbeedce66e5", + "sides": [ + { + "side": "a", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-a-ffponv1z/work/a/clock2", + "elf_sha256": "54f34b65e934efa720f9755179e03e8b0edc6abeb8039f2788a03dbeedce66e5", + "size": 4400, + "embedded_host_strings": [] + }, + { + "side": "bb-longer-path", + "path": "/var/folders/1g/284xdqbn7_338zrkrk4ln6yr0000gn/T/gosvm-rebuild-bb-longer-path-o20y_uj6/work/bb-longer-path/clock2", + "elf_sha256": "54f34b65e934efa720f9755179e03e8b0edc6abeb8039f2788a03dbeedce66e5", + "size": 4400, + "embedded_host_strings": [] + } + ], + "matches_reference": true + } + ], + "backends": [ + "OK fogo-v0 backend (selected) / platform-tools v1.57 / LLVM 22.1.2: ~/Library/Caches/gosvm/toolchains/v1.57/darwin-arm64/llvm", + "OK solana-v3 backend / platform-tools v1.57 / LLVM 22.1.2: ~/Library/Caches/gosvm/toolchains/v1.57/darwin-arm64/llvm" + ], + "unmanaged_v1_57_llvm_sha256": {}, + "reference": "results/compiler/2026-10-15-byval-rebuild/metrics.json", + "passed": true +} diff --git a/scripts/verify_rebuild.py b/scripts/verify_rebuild.py index ab22410b..800bd1ac 100644 --- a/scripts/verify_rebuild.py +++ b/scripts/verify_rebuild.py @@ -22,9 +22,9 @@ def main(): ap.add_argument('--output', type=pathlib.Path, required=True) ap.add_argument('--cli', type=pathlib.Path, default=ROOT / 'build/gosvm') ap.add_argument('--reference', type=pathlib.Path, - default=ROOT / 'results/compiler/2026-10-15-byval-rebuild/metrics.json', + default=ROOT / 'results/compiler/2026-10-19-cpi-limits/metrics.json', help='earlier metrics.json whose ELF hashes are compared (informational); the default is ' - 'the current reference (the October 15 aggregate-parameter fix changed the full swap)') + 'the current reference (the October 19 CPI bounds changed the full swap)') a = ap.parse_args(); a.output = a.output.resolve(); a.cli = a.cli.resolve() if a.output.exists(): sys.exit(f'{a.output} exists; use a new results directory') diff --git a/sdk/cpi/cpi.go b/sdk/cpi/cpi.go index be3d9f46..acc66f67 100644 --- a/sdk/cpi/cpi.go +++ b/sdk/cpi/cpi.go @@ -9,18 +9,26 @@ import ( const ErrMetas uint64 = 3002 const ErrSigners uint64 = 3003 +// MaxMetas and MaxSigners match solana.Invoke: account metas per CPI +// (repeated accounts count) and PDA signer groups per CPI. +const ( + MaxMetas = 64 + MaxSigners = 4 +) + +// InlineMetas records live in the builder itself. The first Add beyond them +// moves the records to one MaxMetas-record array on the invocation heap +// (576 bytes, allocated once), so CPIs of up to 16 metas keep the builder's +// small frame and zeroing cost and never allocate. +const InlineMetas = 16 + type Metas struct { - buffer [144]byte + buffer [InlineMetas * 9]byte count uint64 + wide *[MaxMetas * 9]byte } func (m *Metas) Add(index uint64, writable, signer bool) uint64 { - if m.count >= 16 { - return ErrMetas - } - for j := uint64(0); j < 8; j++ { - m.buffer[m.count*9+j] = byte(index >> (j * 8)) - } flags := byte(0) if writable { flags = flags | 1 @@ -28,12 +36,35 @@ func (m *Metas) Add(index uint64, writable, signer bool) uint64 { if signer { flags = flags | 2 } - m.buffer[m.count*9+8] = flags + if m.count < InlineMetas { + for j := uint64(0); j < 8; j++ { + m.buffer[m.count*9+j] = byte(index >> (j * 8)) + } + m.buffer[m.count*9+8] = flags + } else if m.count < MaxMetas { + if m.wide == nil { + m.wide = new([MaxMetas * 9]byte) + copy(m.wide[:], m.buffer[:]) + } + for j := uint64(0); j < 8; j++ { + m.wide[m.count*9+j] = byte(index >> (j * 8)) + } + m.wide[m.count*9+8] = flags + } else { + return ErrMetas + } m.count++ return 0 } -func (m *Metas) Bytes() []byte { return m.buffer[:m.count*9] } +// Bytes borrows the records: inline, or the heap array after the 17th Add. +// A Metas copied after its records moved shares that array. +func (m *Metas) Bytes() []byte { + if m.count > InlineMetas { + return m.wide[:m.count*9] + } + return m.buffer[:m.count*9] +} type Signers struct { buffer [1024]byte @@ -42,7 +73,7 @@ type Signers struct { } func (s *Signers) Add(seeds *pda.Seeds) uint64 { - if seeds == nil || s.count >= 2 || s.used > 1023 { + if seeds == nil || s.count >= MaxSigners || s.used > 1023 { return ErrSigners } b := seeds.Bytes() @@ -72,7 +103,7 @@ func Invoke(c solana.Context, program uint64, metas *Metas, data []byte, signers // SingleSigner encodes one signer group directly, without a Seeds-to-Signers copy. // It preserves the seed count/length/capacity checks of pda.Seeds. General -// Signers and its two-group/1024-byte checks remain unchanged. +// Signers keeps its four-group/1024-byte checks. // The borrowed encoding must remain alive and unmodified through InvokeSingle. type SingleSigner struct { buffer [530]byte diff --git a/sdk/cpi/cpi_test.go b/sdk/cpi/cpi_test.go index 0375c618..9ac6fb84 100644 --- a/sdk/cpi/cpi_test.go +++ b/sdk/cpi/cpi_test.go @@ -10,27 +10,44 @@ import ( func TestBoundedMetasAndSignerGroups(t *testing.T) { var m Metas - for i := uint64(0); i < 16; i++ { + for i := uint64(0); i < MaxMetas; i++ { if m.Add(i, i%2 == 0, i%3 == 0) != 0 { t.Fatal(i) } + // The first 16 records stay inline; the 17th moves them to the heap. + if (m.wide != nil) != (i >= InlineMetas) || len(m.Bytes()) != int(i+1)*9 { + t.Fatal("inline/heap records", i) + } } b := append([]byte(nil), m.Bytes()...) - if len(b) != 144 || m.Add(0, false, false) != ErrMetas || !bytes.Equal(b, m.Bytes()) { + if len(b) != 576 || m.Add(0, false, false) != ErrMetas || !bytes.Equal(b, m.Bytes()) { t.Fatal("meta bounds") } - for i := 0; i < 16; i++ { - if binary.LittleEndian.Uint64(b[i*9:]) != uint64(i) { - t.Fatal("index encoding") + for i := 0; i < MaxMetas; i++ { + flags := byte(0) + if i%2 == 0 { + flags |= 1 + } + if i%3 == 0 { + flags |= 2 + } + if binary.LittleEndian.Uint64(b[i*9:]) != uint64(i) || b[i*9+8] != flags { + t.Fatal("record encoding", i) } } var a, z pda.Seeds a.AddByte(9) z.AddBytes(nil) var groups Signers - if groups.Add(&a) != 0 || groups.Add(&z) != 0 || !bytes.Equal(groups.Bytes(), []byte{2, 1, 1, 9, 1, 0}) || groups.Add(&a) != ErrSigners { + if groups.Add(&a) != 0 || groups.Add(&z) != 0 || !bytes.Equal(groups.Bytes(), []byte{2, 1, 1, 9, 1, 0}) { t.Fatal("signer groups", groups.Bytes()) } + if groups.Add(&a) != 0 || groups.Add(&z) != 0 || !bytes.Equal(groups.Bytes(), []byte{4, 1, 1, 9, 1, 0, 1, 1, 9, 1, 0}) { + t.Fatal("four signer groups", groups.Bytes()) + } + if groups.Add(&a) != ErrSigners || !bytes.Equal(groups.Bytes(), []byte{4, 1, 1, 9, 1, 0, 1, 1, 9, 1, 0}) { + t.Fatal("fifth signer group", groups.Bytes()) + } var maximum pda.Seeds for i := 0; i < 16; i++ { maximum.AddKey([32]byte{}) @@ -62,10 +79,11 @@ func TestInvokeValidationAndExactCallbackStatus(t *testing.T) { want uint64 }{ {2, m.Bytes(), nil, nil, 2005}, {1, m.Bytes(), nil, nil, 2001}, - {0, []byte{0}, nil, nil, 2006}, {0, bytes.Repeat([]byte{0}, 153), nil, nil, 2006}, + {0, []byte{0}, nil, nil, 2006}, {0, bytes.Repeat([]byte{0}, 65*9), nil, nil, 2006}, {0, bytes.Repeat([]byte{0}, 577), nil, nil, 2006}, {0, []byte{2, 0, 0, 0, 0, 0, 0, 0, 0}, nil, nil, 2006}, {0, []byte{1, 0, 0, 0, 0, 0, 0, 0, 4}, nil, nil, 2006}, {0, nil, make([]byte, 10241), nil, 2008}, {0, nil, nil, []byte{3}, 2007}, {0, nil, nil, []byte{1, 1, 33}, 2007}, {0, nil, nil, []byte{1, 1, 2, 7}, 2007}, {0, nil, nil, []byte{0, 1}, 2007}, + {0, nil, nil, []byte{5, 0, 0, 0, 0, 0}, 2007}, {0, nil, nil, []byte{4, 0, 0, 0}, 2007}, } { if got := solana.Invoke(c, tc.p, tc.m, tc.d, tc.s); got != tc.want { t.Fatalf("got %d want %d", got, tc.want) @@ -74,4 +92,25 @@ func TestInvokeValidationAndExactCallbackStatus(t *testing.T) { if calls != 1 { t.Fatal("invalid inputs reached callback", calls) } + // The maxima reach the callback unchanged: 64 metas and four groups. + var wide Metas + for i := uint64(0); i < MaxMetas; i++ { + wide.Add(i%2, false, false) + } + var seeds pda.Seeds + seeds.AddByte(7) + var four Signers + for i := 0; i < MaxSigners; i++ { + four.Add(&seeds) + } + c.InvokeInstruction = func(_ uint64, metas, _, signers []byte) uint64 { + calls++ + if !bytes.Equal(metas, wide.Bytes()) || !bytes.Equal(signers, four.Bytes()) || len(metas) != 576 || signers[0] != 4 { + t.Fatal(len(metas), signers) + } + return 0 + } + if code := Invoke(c, 0, &wide, nil, &four); code != 0 || calls != 2 { + t.Fatal(code, calls) + } } diff --git a/solana/api.go b/solana/api.go index f094d48f..aa5d7556 100644 --- a/solana/api.go +++ b/solana/api.go @@ -249,7 +249,8 @@ func Lamports(c Context, i uint64) uint64 { return c.Accounts[i].Lamports } // groups. Applications should use sdk/cpi builders instead of assembling these // encodings. Meta records are index:uint64 LE and flags:byte (writable=1, // signer=2). Signer groups are count:byte, then seed-count:byte followed by each -// length:byte and its bytes. Limits: 16 metas, 2 groups, 16 seeds/group, 32/seed. +// length:byte and its bytes. Limits: 64 metas (duplicates count), 4 groups, +// 16 seeds/group, 32 bytes/seed and 1,024 encoded signer bytes. func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if program >= uint64(len(c.Accounts)) { return 2005 @@ -257,7 +258,7 @@ func Invoke(c Context, program uint64, metas, data, signerSeeds []byte) uint64 { if !c.Accounts[program].Executable { return 2001 } - if len(metas)%9 != 0 || len(metas) > 16*9 { + if len(metas)%9 != 0 || len(metas) > 64*9 { return 2006 } for off := 0; off < len(metas); off += 9 { @@ -305,7 +306,7 @@ func validSignerSeeds(b []byte) bool { if len(b) == 0 { return true } - if len(b) > 1024 || b[0] > 2 { + if len(b) > 1024 || b[0] > 4 { return false } off := 1