@typesol/cli 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/compiler/analyze.js +62 -5
- package/dist/compiler/emit/base.d.ts +2 -0
- package/dist/compiler/emit/base.js +4 -1
- package/dist/compiler/emit/calls.d.ts +16 -1
- package/dist/compiler/emit/calls.js +129 -1
- package/dist/compiler/emit/expressions.js +18 -0
- package/dist/compiler/emit/function.js +28 -0
- package/dist/compiler/emit/shared.js +2 -0
- package/dist/compiler/emit/storage.js +18 -0
- package/dist/compiler/model.d.ts +12 -0
- package/dist/compiler/model.js +3 -0
- package/docs/case-study-escrow.md +3 -3
- package/docs/errors.md +1 -1
- package/docs/language.md +7 -2
- package/docs/migrate-from-anchor.md +4 -0
- package/docs/security-model.md +11 -3
- package/lib/typesol.d.ts +55 -1
- package/package.json +1 -1
- package/runtime/typesol_rt.h +82 -21
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ codes, and says so here.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.27.0 — 2026-10-29
|
|
10
|
+
|
|
11
|
+
Anchor 1.0 parity: instruction introspection, `Dup<…>` and account migration. Existing programs compile to the same
|
|
12
|
+
binaries as in 0.26.0.
|
|
13
|
+
|
|
14
|
+
- New: instruction introspection. `InstructionsSysvar` has `length`, `currentIndex()` and `get(i)`, a
|
|
15
|
+
`TransactionInstruction` with `programId`, its accounts (key, signer, writable), its data
|
|
16
|
+
(`readU8/U16/U32/U64/readPubkey`) and `is(program, "name")` (an instruction by its Anchor discriminator); every read
|
|
17
|
+
is bounds-checked (5104). `stackHeight()` is 1 at the top level, more under a CPI.
|
|
18
|
+
- New: `Dup<…>`, Anchor's `dup`: a writable parameter that may be the same account as another, for accounts read and
|
|
19
|
+
written in place (not created, closed, a close destination or `@layout borsh`, which are refused at compile time).
|
|
20
|
+
- New: `Migrate<From, To>`, Anchor 1.0's `Migration`: an existing account rewritten from one struct to another with
|
|
21
|
+
`acct.migrate(payer, value)`, the old data readable through `acct.from` until then (the new value is built before
|
|
22
|
+
the bytes are rewritten), the new through `acct.to` after; resized with the payer topping up rent. For now:
|
|
23
|
+
fixed-layout structs without growable Vecs, no constraints on the account, and it cannot be passed to a helper.
|
|
24
|
+
- New: `/** @discriminator … */` on an account struct (a sha256 preimage such as `account:Counter`, or 0x and 8
|
|
25
|
+
bytes), Anchor's `#[account(discriminator = …)]`: keeps an old type's on-chain discriminator under a new name.
|
|
26
|
+
|
|
9
27
|
## 0.26.0 — 2026-10-29
|
|
10
28
|
|
|
11
29
|
A first-run experience pass, the security model and audit package, and more fuzzing. The compiler's output is the
|
package/dist/compiler/analyze.js
CHANGED
|
@@ -85,6 +85,8 @@ const TYPE_EXPORTS = new Set([
|
|
|
85
85
|
"Ata",
|
|
86
86
|
"Owner",
|
|
87
87
|
"Executable",
|
|
88
|
+
"Dup",
|
|
89
|
+
"Migrate",
|
|
88
90
|
"Optional",
|
|
89
91
|
"EventAuthority",
|
|
90
92
|
"MplCoreProgram",
|
|
@@ -114,6 +116,22 @@ export function snakeCase(name) {
|
|
|
114
116
|
.replace(/([A-Z])([A-Z][a-z])/g, "$1_$2")
|
|
115
117
|
.toLowerCase();
|
|
116
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* An account struct's discriminator: Anchor's sha256("account:<Name>")[..8], or the one a `/** @discriminator … *\/` tag
|
|
121
|
+
* gives (a sha256 preimage such as `account:Counter`, or 0x and 8 bytes), e.g. for an old version of a struct kept under
|
|
122
|
+
* a new name for Migrate<From, To>.
|
|
123
|
+
*/
|
|
124
|
+
function accountDiscriminator(decl, name, reporter) {
|
|
125
|
+
const tag = ts.getJSDocTags(decl).find((t) => t.tagName.text === "discriminator");
|
|
126
|
+
if (!tag)
|
|
127
|
+
return discriminator(`account:${name}`);
|
|
128
|
+
const text = (typeof tag.comment === "string" ? tag.comment : (ts.getTextOfJSDocComment(tag.comment) ?? "")).trim();
|
|
129
|
+
if (/^0x([0-9a-fA-F]{2}){8}$/.test(text))
|
|
130
|
+
return [...Buffer.from(text.slice(2), "hex")];
|
|
131
|
+
if (text && !text.startsWith("0x"))
|
|
132
|
+
return discriminator(text);
|
|
133
|
+
reporter.fail(decl, "@discriminator on an account struct needs a sha256 preimage (account:<Name>) or 0x and 8 bytes");
|
|
134
|
+
}
|
|
117
135
|
export function discriminator(preimage) {
|
|
118
136
|
return [...createHash("sha256").update(preimage).digest().subarray(0, 8)];
|
|
119
137
|
}
|
|
@@ -601,12 +619,17 @@ export function analyze(entry) {
|
|
|
601
619
|
// A returned struct goes out in its fixed layout, like an argument comes in: it cannot also be a Borsh event.
|
|
602
620
|
if (ix.returns?.kind === "struct")
|
|
603
621
|
collectStructs(ix.returns, structs, dataEnums, argStructs);
|
|
604
|
-
for (const a of ix.accounts)
|
|
622
|
+
for (const a of ix.accounts) {
|
|
623
|
+
if (a.kind.kind === "migrate") {
|
|
624
|
+
accountStructs.add(a.kind.from.name);
|
|
625
|
+
accountStructs.add(a.kind.to.name);
|
|
626
|
+
}
|
|
605
627
|
if (a.kind.kind === "account" || a.kind.kind === "init" || a.kind.kind === "zeroed" || a.kind.kind === "initIfNeeded") {
|
|
606
628
|
accountStructs.add(a.kind.struct.name);
|
|
607
629
|
if (a.kind.struct.viewsOnly)
|
|
608
630
|
reporter.error(a.node, `${a.kind.struct.name} holds a string (or an enum carrying one), so it is an argument, return value or event; account data uses FixedString<N>`);
|
|
609
631
|
}
|
|
632
|
+
}
|
|
610
633
|
}
|
|
611
634
|
// Arguments are Borsh, where a FixedVec is a Vec (only what it holds). A struct passed as a whole argument and used
|
|
612
635
|
// nowhere else (not account data, not nested, not returned) is read as Anchor sends it: @layout borsh, implied.
|
|
@@ -966,7 +989,7 @@ function analyzeStructs(items, aliases, dataEnums, reporter, scopeFor, fstrings,
|
|
|
966
989
|
name,
|
|
967
990
|
fields: stored,
|
|
968
991
|
size: undefined,
|
|
969
|
-
accountDiscriminator:
|
|
992
|
+
accountDiscriminator: accountDiscriminator(decl, name, reporter),
|
|
970
993
|
eventDiscriminator: discriminator(`event:${name}`),
|
|
971
994
|
node: decl,
|
|
972
995
|
layout: layoutText === "borsh" ? "borsh" : "fixed",
|
|
@@ -1321,7 +1344,7 @@ function analyzeHelper(fn, name, display, reporter, scope, structs, skip = new S
|
|
|
1321
1344
|
wrapped === "Ata")
|
|
1322
1345
|
reporter.fail(p.type, "constraints belong on instruction parameters; helpers take the plain account type");
|
|
1323
1346
|
const account = resolveAccountKind(p.type, reporter, scope.imports, structs);
|
|
1324
|
-
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed")
|
|
1347
|
+
if (account.kind === "init" || account.kind === "initIfNeeded" || account.kind === "zeroed" || account.kind === "migrate")
|
|
1325
1348
|
reporter.fail(p.type, "helpers take accounts that exist: create Init/InitIfNeeded/Zeroed accounts in the instruction, then pass them as Mut<T>");
|
|
1326
1349
|
return { name: p.name.text, type: { kind: "account" }, account, node: p };
|
|
1327
1350
|
});
|
|
@@ -1445,7 +1468,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1445
1468
|
const inner = unwrapConstraints(typeNode, reporter, imports, constraints);
|
|
1446
1469
|
const kind = resolveAccountKind(inner, reporter, imports, structs);
|
|
1447
1470
|
if (optional &&
|
|
1448
|
-
(["init", "initIfNeeded", "zeroed", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1471
|
+
(["init", "initIfNeeded", "zeroed", "migrate", "pyth", "eventAuthority", "selfProgram", "programData"].includes(kind.kind) ||
|
|
1449
1472
|
((kind.kind === "token" || kind.kind === "mint") && kind.init)))
|
|
1450
1473
|
reporter.fail(p, "Init, InitIfNeeded, Zeroed, PythPriceUpdate, EventAuthority, SelfProgram and ProgramData accounts cannot be optional");
|
|
1451
1474
|
accounts.push({ name: p.name.text, index: accounts.length, kind, node: p, constraints, ...(optional ? { optional } : {}) });
|
|
@@ -1566,6 +1589,18 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1566
1589
|
reporter.error(fn.name, "instructions that grow a Vec (push) need a SystemProgram parameter to top up rent");
|
|
1567
1590
|
if (needs("init") && !needs("system"))
|
|
1568
1591
|
reporter.error(fn.name, "instructions with Init<...> accounts need a SystemProgram parameter");
|
|
1592
|
+
if (accounts.some((a) => a.kind.kind === "migrate" && a.kind.to.size > a.kind.from.size) && !needs("system"))
|
|
1593
|
+
reporter.error(fn.name, "a Migrate<From, To> that grows the account needs a SystemProgram parameter (the payer tops up its rent)");
|
|
1594
|
+
for (const a of accounts)
|
|
1595
|
+
if (a.kind.kind === "migrate" &&
|
|
1596
|
+
(a.constraints.hasOne.length ||
|
|
1597
|
+
a.constraints.seeds ||
|
|
1598
|
+
a.constraints.closeTo ||
|
|
1599
|
+
a.constraints.ata ||
|
|
1600
|
+
a.constraints.address ||
|
|
1601
|
+
a.constraints.owner ||
|
|
1602
|
+
a.constraints.dup))
|
|
1603
|
+
reporter.error(a.node, "Migrate<From, To> takes no constraints yet: check the old fields with require(…, …) on acct.from");
|
|
1569
1604
|
const name = snakeCase(fn.name.text);
|
|
1570
1605
|
// `/** @discriminator spl-transfer-hook-interface:execute */` (sha256 preimage) or `@discriminator 0x...` (raw bytes)
|
|
1571
1606
|
// lets an instruction implement another program's interface.
|
|
@@ -1596,7 +1631,7 @@ function analyzeInstruction(fn, reporter, scope, structs) {
|
|
|
1596
1631
|
/** Peel HasOne / Seeds / CloseTo wrappers off a parameter type, recording what they ask for. */
|
|
1597
1632
|
function unwrapConstraints(node, reporter, imports, out) {
|
|
1598
1633
|
const name = typesolName(node, imports);
|
|
1599
|
-
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable"].includes(name ?? ""))
|
|
1634
|
+
if (!["HasOne", "TokenOf", "MintOf", "Seeds", "CloseTo", "Address", "Ata", "Owner", "Executable", "Dup"].includes(name ?? ""))
|
|
1600
1635
|
return node;
|
|
1601
1636
|
const args = node.typeArguments ?? [];
|
|
1602
1637
|
const lit = (t) => {
|
|
@@ -1638,6 +1673,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1638
1673
|
}
|
|
1639
1674
|
if (name === "Executable")
|
|
1640
1675
|
out.executable = true;
|
|
1676
|
+
if (name === "Dup")
|
|
1677
|
+
out.dup = true;
|
|
1641
1678
|
if (name === "Address") {
|
|
1642
1679
|
const text = lit(args[1])[0];
|
|
1643
1680
|
const bytes = text === undefined ? undefined : decodeAddress(text);
|
|
@@ -1668,6 +1705,8 @@ function unwrapConstraints(node, reporter, imports, out) {
|
|
|
1668
1705
|
reporter.fail(node, `${name}<…> wraps an account type`);
|
|
1669
1706
|
return unwrapConstraints(args[0], reporter, imports, out);
|
|
1670
1707
|
}
|
|
1708
|
+
/** Solana: an account grows at most this many bytes per instruction. */
|
|
1709
|
+
const MAX_PERMITTED_DATA_INCREASE = 10_240;
|
|
1671
1710
|
function resolveAccountKind(node, reporter, imports, structs) {
|
|
1672
1711
|
const outer = typesolName(node, imports);
|
|
1673
1712
|
const ref = node;
|
|
@@ -1769,6 +1808,24 @@ function resolveAccountKind(node, reporter, imports, structs) {
|
|
|
1769
1808
|
reporter.fail(node, `${s.name} is too large to create in a program; use Zeroed<${s.name}>`);
|
|
1770
1809
|
return { kind: "initIfNeeded", struct: s };
|
|
1771
1810
|
}
|
|
1811
|
+
case "Migrate": {
|
|
1812
|
+
const [fromNode, toNode] = ref.typeArguments ?? [];
|
|
1813
|
+
const from = fromNode && structOf(fromNode);
|
|
1814
|
+
const to = toNode && structOf(toNode);
|
|
1815
|
+
if (!from || !to || ref.typeArguments.length !== 2)
|
|
1816
|
+
reporter.fail(node, "Migrate<From, To> takes two account structs declared in this program");
|
|
1817
|
+
for (const s of [from, to]) {
|
|
1818
|
+
if (s.vec)
|
|
1819
|
+
reporter.fail(node, `${s.name} has a growable Vec; Migrate<From, To> takes fixed-size structs`);
|
|
1820
|
+
if (s.layout === "borsh")
|
|
1821
|
+
reporter.fail(node, `${s.name} is @layout borsh; Migrate<From, To> takes fixed layouts`);
|
|
1822
|
+
}
|
|
1823
|
+
if (from.accountDiscriminator.join() === to.accountDiscriminator.join())
|
|
1824
|
+
reporter.fail(node, `${from.name} and ${to.name} have the same discriminator: give the new struct another name (or the old one /** @discriminator account:<OldName> */)`);
|
|
1825
|
+
if (to.size - from.size > MAX_PERMITTED_DATA_INCREASE)
|
|
1826
|
+
reporter.fail(node, `${to.name} is more than 10 KiB larger than ${from.name}; an account grows at most 10 KiB per instruction`);
|
|
1827
|
+
return { kind: "migrate", from, to };
|
|
1828
|
+
}
|
|
1772
1829
|
case "Zeroed": {
|
|
1773
1830
|
const s = structOf(inner());
|
|
1774
1831
|
if (!s)
|
|
@@ -112,6 +112,8 @@ export declare abstract class EmitterBase {
|
|
|
112
112
|
protected abstract call(node: ts.CallExpression, topLevel: boolean): Value | undefined;
|
|
113
113
|
protected abstract isRemaining(node: ts.Expression): boolean;
|
|
114
114
|
protected abstract handle(node: ts.Expression): string | undefined;
|
|
115
|
+
protected abstract instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
116
|
+
protected abstract isTxIx(node: ts.Expression): boolean;
|
|
115
117
|
protected abstract seeds(node: ts.Expression, extra: number): {
|
|
116
118
|
name: string;
|
|
117
119
|
count: number;
|
|
@@ -65,7 +65,10 @@ export class EmitterBase {
|
|
|
65
65
|
this.ix = target;
|
|
66
66
|
for (const a of target.accounts) {
|
|
67
67
|
this.accounts.set(a.name, a);
|
|
68
|
-
if (a.kind.kind === "init" ||
|
|
68
|
+
if (a.kind.kind === "init" ||
|
|
69
|
+
a.kind.kind === "initIfNeeded" ||
|
|
70
|
+
a.kind.kind === "migrate" ||
|
|
71
|
+
((a.kind.kind === "token" || a.kind.kind === "mint") && a.kind.init))
|
|
69
72
|
this.pendingInit.add(a.name);
|
|
70
73
|
}
|
|
71
74
|
for (const a of target.args)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
2
|
+
import { type AccountParam, type ErrorDef, type FieldType, type Helper } from "../model.js";
|
|
3
3
|
import { type Value, type Ref, type Closure } from "./shared.js";
|
|
4
4
|
import { ExpressionEmitter } from "./expressions.js";
|
|
5
5
|
export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
@@ -20,6 +20,15 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
20
20
|
/** `remainingAccounts()` or a const bound to it. */
|
|
21
21
|
protected isRemaining(node: ts.Expression): boolean;
|
|
22
22
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
23
|
+
/** Whether `node` is an InstructionsSysvar parameter. */
|
|
24
|
+
protected instructionsParam(node: ts.Expression): AccountParam | undefined;
|
|
25
|
+
/** Whether `node` is a TransactionInstruction: a local holding one, or `ixs.get(i)`. */
|
|
26
|
+
protected isTxIx(node: ts.Expression): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* InstructionsSysvar.currentIndex() / .get(i) and the methods of a TransactionInstruction: bounds-checked reads of the
|
|
29
|
+
* sysvar (ts_ix_* in the runtime). Undefined when the call is something else.
|
|
30
|
+
*/
|
|
31
|
+
protected instructionsCall(node: ts.CallExpression, callee: ts.PropertyAccessExpression, method: string): Value | undefined;
|
|
23
32
|
protected handle(node: ts.Expression): string | undefined;
|
|
24
33
|
/** A declared account parameter or a remaining account, as a `TsAccount *` C expression. */
|
|
25
34
|
protected anyAccount(node: ts.Expression, what: string): string;
|
|
@@ -29,6 +38,12 @@ export declare abstract class CallEmitter extends ExpressionEmitter {
|
|
|
29
38
|
protected loadForeign(node: ts.CallExpression): Value;
|
|
30
39
|
/** `loadAccount<T>(acct)` / `loadAccountMut<T>(acct)`: checked view of a program account. */
|
|
31
40
|
protected loadAccount(node: ts.CallExpression, mut: boolean): Value;
|
|
41
|
+
/**
|
|
42
|
+
* `acct.migrate(payer, value)` on a Migrate<From, To> account: the new value is built in full first (it may read
|
|
43
|
+
* acct.from, whose bytes the rewrite overwrites), then the account is resized (the payer tops up the rent when it
|
|
44
|
+
* grows; shrinking keeps the lamports) and rewritten with To's discriminator and the value.
|
|
45
|
+
*/
|
|
46
|
+
protected migrate(node: ts.CallExpression, a: AccountParam, topLevel: boolean): Value | undefined;
|
|
32
47
|
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
33
48
|
private helperGrows;
|
|
34
49
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { calleeName, resolveScalarType, resolveValueType } from "../analyze.js";
|
|
2
|
+
import { calleeName, discriminator, resolveScalarType, resolveValueType, snakeCase } from "../analyze.js";
|
|
3
3
|
import { ANCHOR_ERRORS } from "../anchor-errors.js";
|
|
4
4
|
import { decodeAddress } from "../../keys.js";
|
|
5
5
|
import { INT_INFO, INT_NAMES, isScalar, isWritable, sameType, scalarSize, typeName, } from "../model.js";
|
|
@@ -21,6 +21,11 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
21
21
|
return this.cast(args[0], { kind: "int", name: builtin });
|
|
22
22
|
}
|
|
23
23
|
switch (builtin) {
|
|
24
|
+
case "stackHeight": {
|
|
25
|
+
if (args.length)
|
|
26
|
+
this.fail(node, "stackHeight() takes no arguments");
|
|
27
|
+
return { c: "sol_get_stack_height()", type: U64 };
|
|
28
|
+
}
|
|
24
29
|
case "require": {
|
|
25
30
|
if (args.length !== 2)
|
|
26
31
|
this.fail(node, "require(condition, error) takes two arguments");
|
|
@@ -484,6 +489,9 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
484
489
|
return this.callHelper(node, helper);
|
|
485
490
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
486
491
|
const method = callee.name.text;
|
|
492
|
+
const introspected = this.instructionsCall(node, callee, method);
|
|
493
|
+
if (introspected)
|
|
494
|
+
return introspected;
|
|
487
495
|
// xs.some(…) and friends on a Vec, FixedArray, FixedVec or Vec argument: a loop with the callback expanded in it.
|
|
488
496
|
if (LIST_METHODS.has(method)) {
|
|
489
497
|
const l = ts.isIdentifier(callee.expression) ? this.lookup(callee.expression.text) : undefined;
|
|
@@ -570,6 +578,8 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
570
578
|
return target.kind.kind === "token" && target.constraints.ata ? this.createAta(node, target, topLevel) : this.createToken(node, target, topLevel);
|
|
571
579
|
if (target?.kind.kind === "init" && method === "create")
|
|
572
580
|
return this.create(node, target, topLevel);
|
|
581
|
+
if (target?.kind.kind === "migrate" && method === "migrate")
|
|
582
|
+
return this.migrate(node, target, topLevel);
|
|
573
583
|
if (target?.kind.kind === "initIfNeeded" && method === "createIfNeeded")
|
|
574
584
|
return this.create(node, target, topLevel, true);
|
|
575
585
|
if (target?.kind.kind === "system" && method === "transfer")
|
|
@@ -878,6 +888,92 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
878
888
|
return ts.isCallExpression(node) && this.exportName(node.expression) === "remainingAccounts";
|
|
879
889
|
}
|
|
880
890
|
/** C pointer (`TsAccount *`) for an expression naming a remaining account, or undefined. */
|
|
891
|
+
/** Whether `node` is an InstructionsSysvar parameter. */
|
|
892
|
+
instructionsParam(node) {
|
|
893
|
+
const a = ts.isIdentifier(node) ? this.accounts.get(node.text) : undefined;
|
|
894
|
+
return a?.kind.kind === "instructionsSysvar" ? a : undefined;
|
|
895
|
+
}
|
|
896
|
+
/** Whether `node` is a TransactionInstruction: a local holding one, or `ixs.get(i)`. */
|
|
897
|
+
isTxIx(node) {
|
|
898
|
+
if (ts.isParenthesizedExpression(node))
|
|
899
|
+
return this.isTxIx(node.expression);
|
|
900
|
+
if (ts.isIdentifier(node))
|
|
901
|
+
return this.lookup(node.text)?.type.kind === "txIx";
|
|
902
|
+
return (ts.isCallExpression(node) &&
|
|
903
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
904
|
+
node.expression.name.text === "get" &&
|
|
905
|
+
!!this.instructionsParam(node.expression.expression));
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* InstructionsSysvar.currentIndex() / .get(i) and the methods of a TransactionInstruction: bounds-checked reads of the
|
|
909
|
+
* sysvar (ts_ix_* in the runtime). Undefined when the call is something else.
|
|
910
|
+
*/
|
|
911
|
+
instructionsCall(node, callee, method) {
|
|
912
|
+
const args = node.arguments;
|
|
913
|
+
const sysvar = this.instructionsParam(callee.expression);
|
|
914
|
+
if (sysvar) {
|
|
915
|
+
this.assertUsable(sysvar, callee.expression);
|
|
916
|
+
if (method === "currentIndex") {
|
|
917
|
+
if (args.length)
|
|
918
|
+
this.fail(node, "currentIndex() takes no arguments");
|
|
919
|
+
const t = this.tmp("ixi");
|
|
920
|
+
this.line(`uint64_t ${t};`);
|
|
921
|
+
this.line(`TS_TRY(ts_ix_current(a_${sysvar.name}, &${t}));`);
|
|
922
|
+
return { c: t, type: U64 };
|
|
923
|
+
}
|
|
924
|
+
if (method === "get") {
|
|
925
|
+
if (args.length !== 1)
|
|
926
|
+
this.fail(node, "get(index) takes the instruction's index");
|
|
927
|
+
const i = this.expect(this.expr(args[0], U64), U64, args[0]);
|
|
928
|
+
const t = this.tmp("ix");
|
|
929
|
+
this.line(`TsIx ${t};`);
|
|
930
|
+
this.line(`TS_TRY(ts_ix_load(a_${sysvar.name}, ${i.c}, &${t}));`);
|
|
931
|
+
return { c: t, type: { kind: "txIx" } };
|
|
932
|
+
}
|
|
933
|
+
this.fail(node, "the Instructions sysvar has .length, currentIndex() and get(index)");
|
|
934
|
+
}
|
|
935
|
+
if (!this.isTxIx(callee.expression))
|
|
936
|
+
return undefined;
|
|
937
|
+
const ix = this.expr(callee.expression);
|
|
938
|
+
const one = (what) => {
|
|
939
|
+
if (args.length !== 1)
|
|
940
|
+
this.fail(node, `${method}(${what}) takes one argument`);
|
|
941
|
+
return this.expect(this.expr(args[0], U64), U64, args[0]).c;
|
|
942
|
+
};
|
|
943
|
+
if (method === "accountKey" || method === "accountIsSigner" || method === "accountIsWritable") {
|
|
944
|
+
const i = one("i");
|
|
945
|
+
const t = this.tmp("ixa");
|
|
946
|
+
this.line(`const uint8_t *${t};`);
|
|
947
|
+
this.line(`TS_TRY(ts_ix_account(&${ix.c}, ${i}, &${t}));`);
|
|
948
|
+
if (method === "accountKey")
|
|
949
|
+
return { c: `((const SolPubkey *)(${t} + 1))`, type: PUBKEY };
|
|
950
|
+
return { c: `((${t}[0] & ${method === "accountIsSigner" ? 1 : 2}) != 0)`, type: BOOL };
|
|
951
|
+
}
|
|
952
|
+
const reads = {
|
|
953
|
+
readU8: ["uint8_t", U8],
|
|
954
|
+
readU16: ["uint16_t", { kind: "int", name: "u16" }],
|
|
955
|
+
readU32: ["uint32_t", { kind: "int", name: "u32" }],
|
|
956
|
+
readU64: ["uint64_t", U64],
|
|
957
|
+
readPubkey: ["SolPubkey", PUBKEY],
|
|
958
|
+
};
|
|
959
|
+
if (reads[method]) {
|
|
960
|
+
const [c, type] = reads[method];
|
|
961
|
+
const off = one("offset");
|
|
962
|
+
const t = this.tmp("ixr");
|
|
963
|
+
this.line(`${c} ${t};`);
|
|
964
|
+
this.line(`TS_TRY(ts_ix_read(&${ix.c}, ${off}, sizeof(${c}), &${t}));`);
|
|
965
|
+
return type.kind === "pubkey" ? { c: `(&${t})`, type } : { c: t, type };
|
|
966
|
+
}
|
|
967
|
+
if (method === "is") {
|
|
968
|
+
if (args.length !== 2 || !ts.isStringLiteralLike(args[1]))
|
|
969
|
+
this.fail(node, 'is(program, "instruction_name") takes the program and the instruction name as a string literal');
|
|
970
|
+
const program = this.expect(this.expr(args[0], PUBKEY), PUBKEY, args[0]);
|
|
971
|
+
const disc = this.tmp("disc");
|
|
972
|
+
this.line(`static const uint8_t ${disc}[8] = ${cBytes(discriminator(`global:${snakeCase(args[1].text)}`))};`);
|
|
973
|
+
return { c: `ts_ix_is(&${ix.c}, ${program.c}, ${disc})`, type: BOOL };
|
|
974
|
+
}
|
|
975
|
+
this.fail(node, "a TransactionInstruction has programId, accountsLength, dataLength, accountKey/accountIsSigner/accountIsWritable(i), readU8/U16/U32/U64/readPubkey(offset) and is(program, name)");
|
|
976
|
+
}
|
|
881
977
|
handle(node) {
|
|
882
978
|
if (ts.isParenthesizedExpression(node))
|
|
883
979
|
return this.handle(node.expression);
|
|
@@ -984,6 +1080,38 @@ export class CallEmitter extends ExpressionEmitter {
|
|
|
984
1080
|
}
|
|
985
1081
|
return { c: `((TSS_${s.name} *)(${h}->data + 8))`, type: { kind: "structRef", struct: s.name, writable: mut } };
|
|
986
1082
|
}
|
|
1083
|
+
/**
|
|
1084
|
+
* `acct.migrate(payer, value)` on a Migrate<From, To> account: the new value is built in full first (it may read
|
|
1085
|
+
* acct.from, whose bytes the rewrite overwrites), then the account is resized (the payer tops up the rent when it
|
|
1086
|
+
* grows; shrinking keeps the lamports) and rewritten with To's discriminator and the value.
|
|
1087
|
+
*/
|
|
1088
|
+
migrate(node, a, topLevel) {
|
|
1089
|
+
const k = a.kind;
|
|
1090
|
+
const call = `${a.name}.migrate`;
|
|
1091
|
+
if (!topLevel)
|
|
1092
|
+
this.fail(node, `${call}(…) must be a top-level statement of the instruction`);
|
|
1093
|
+
if (this.created.has(a.name))
|
|
1094
|
+
this.fail(node, `${a.name} is already migrated`);
|
|
1095
|
+
if (node.arguments.length !== 2)
|
|
1096
|
+
this.fail(node, `${call}(payer, value) takes the payer and the new ${k.to.name}`);
|
|
1097
|
+
const payer = this.accountParam(node.arguments[0], "the payer");
|
|
1098
|
+
if (payer.kind.kind !== "signer" || !payer.kind.mut)
|
|
1099
|
+
this.fail(node.arguments[0], "the payer must be a Mut<Signer>");
|
|
1100
|
+
this.assertNotAliased([a.name], node, "migrate");
|
|
1101
|
+
const To = k.to.name;
|
|
1102
|
+
const tmp = this.tmp("next");
|
|
1103
|
+
this.line(`TSS_${To} *${tmp} = NULL;`);
|
|
1104
|
+
this.line(`TS_TRY(ts_alloc(sizeof(TSS_${To}), (uint8_t **)&${tmp}));`);
|
|
1105
|
+
this.writeAggregate({ lv: `(*${tmp})`, type: { kind: "struct", name: To }, writable: true }, node.arguments[1]);
|
|
1106
|
+
this.line(`TS_TRY(ts_resize(params, a_${payer.name}, a_${a.name}, 8 + sizeof(TSS_${To}), entry_${a.name}));`);
|
|
1107
|
+
this.line(`ts_memcpy(a_${a.name}->data, TS_DISC_${To}, 8);`);
|
|
1108
|
+
this.line(`ts_memcpy(a_${a.name}->data + 8, ${tmp}, sizeof(TSS_${To}));`);
|
|
1109
|
+
this.line(`TSS_${To} *dt_${a.name} = (TSS_${To} *)(a_${a.name}->data + 8);`);
|
|
1110
|
+
this.line(`(void)dt_${a.name};`);
|
|
1111
|
+
this.created.add(a.name);
|
|
1112
|
+
this.pendingInit.delete(a.name);
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
987
1115
|
/** Whether a helper (or one it calls) grows a Vec, which needs the instruction's SystemProgram for rent. */
|
|
988
1116
|
helperGrows(body, seen = new Set()) {
|
|
989
1117
|
if (seen.has(body))
|
|
@@ -265,6 +265,14 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
265
265
|
if (field === "length") {
|
|
266
266
|
if (this.isRemaining(node.expression))
|
|
267
267
|
return { c: `((uint64_t)(params->ka_num - ${this.ix.accounts.length}))`, type: U64 };
|
|
268
|
+
const sysvar = this.instructionsParam(node.expression);
|
|
269
|
+
if (sysvar) {
|
|
270
|
+
this.assertUsable(sysvar, node.expression);
|
|
271
|
+
const t = this.tmp("ixn");
|
|
272
|
+
this.line(`uint64_t ${t};`);
|
|
273
|
+
this.line(`TS_TRY(ts_ix_count(a_${sysvar.name}, &${t}));`);
|
|
274
|
+
return { c: t, type: U64 };
|
|
275
|
+
}
|
|
268
276
|
// The number of elements a map / filter chain gives: counted, nothing stored.
|
|
269
277
|
if (this.isPipeline(node.expression)) {
|
|
270
278
|
const n = this.tmp("count");
|
|
@@ -284,6 +292,16 @@ export class ExpressionEmitter extends StorageEmitter {
|
|
|
284
292
|
}
|
|
285
293
|
this.fail(node, "use byteLength(s) for strings; .length is available on arrays, Vecs and remainingAccounts()");
|
|
286
294
|
}
|
|
295
|
+
if (this.isTxIx(node.expression)) {
|
|
296
|
+
const ix = this.expr(node.expression);
|
|
297
|
+
if (field === "programId")
|
|
298
|
+
return { c: `(${ix.c}.program_id)`, type: PUBKEY };
|
|
299
|
+
if (field === "accountsLength")
|
|
300
|
+
return { c: `((uint64_t)${ix.c}.num_accounts)`, type: U64 };
|
|
301
|
+
if (field === "dataLength")
|
|
302
|
+
return { c: `((uint64_t)${ix.c}.data_len)`, type: U64 };
|
|
303
|
+
this.fail(node.name, `a TransactionInstruction has programId, accountsLength and dataLength, not ${field}`);
|
|
304
|
+
}
|
|
287
305
|
const h = this.handle(node.expression);
|
|
288
306
|
if (h) {
|
|
289
307
|
switch (field) {
|
|
@@ -90,6 +90,9 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
90
90
|
}
|
|
91
91
|
head.push(` TS_TRY(ts_check_executable(${v}));`);
|
|
92
92
|
}
|
|
93
|
+
// Migrate<From, To>: an existing From account (owner, discriminator, size), kept to be rewritten as To.
|
|
94
|
+
if (k.kind === "migrate")
|
|
95
|
+
head.push(` TS_TRY(ts_check_program_account(${v}, params->program_id, TS_DISC_${k.from.name}, sizeof(TSS_${k.from.name})));`, ` const TSS_${k.from.name} *df_${a.name} = (const TSS_${k.from.name} *)(${v}->data + 8);`, ` uint64_t entry_${a.name} = ${v}->data_len;`);
|
|
93
96
|
if (k.kind === "zeroed")
|
|
94
97
|
head.push(` TS_TRY(ts_init_zeroed(${v}, params->program_id, TS_DISC_${k.struct.name}, sizeof(TSS_${k.struct.name})));`);
|
|
95
98
|
if (k.kind === "initIfNeeded" && k.struct.vec)
|
|
@@ -152,9 +155,30 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
152
155
|
// The loader passes each account once and marks repeats as duplicates, which share its record (ts_deserialize),
|
|
153
156
|
// so two parameters name the same account exactly when their pointers are equal.
|
|
154
157
|
const writable = ix.accounts.filter((a) => isWritable(a.kind));
|
|
158
|
+
// Dup<…> (Anchor's `dup`) lifts the check for a pair when both accounts are read and written in place: not created
|
|
159
|
+
// here, not closed or a close destination, not a @layout borsh heap copy (a lost update when packed back).
|
|
160
|
+
const closing = new Set(ix.accounts.flatMap((a) => (a.constraints.closeTo ? [a.name, a.constraints.closeTo] : [])));
|
|
161
|
+
const inPlace = (a) => {
|
|
162
|
+
const k = a.kind;
|
|
163
|
+
if (closing.has(a.name))
|
|
164
|
+
return false;
|
|
165
|
+
if (k.kind === "account")
|
|
166
|
+
return k.struct.layout !== "borsh";
|
|
167
|
+
if (k.kind === "token" || k.kind === "mint")
|
|
168
|
+
return !k.init;
|
|
169
|
+
return k.kind === "signer" || k.kind === "info";
|
|
170
|
+
};
|
|
171
|
+
for (const a of writable)
|
|
172
|
+
if (a.constraints.dup && !inPlace(a))
|
|
173
|
+
this.reporter.fail(a.node, "Dup<…> is for accounts read and written in place: not created, closed, a close destination or @layout borsh");
|
|
174
|
+
for (const a of ix.accounts)
|
|
175
|
+
if (a.constraints.dup && !isWritable(a.kind))
|
|
176
|
+
this.reporter.fail(a.node, "Dup<…> lets a writable account alias another; this one is not writable");
|
|
155
177
|
const pairs = [];
|
|
156
178
|
for (let i = 0; i < writable.length; i++)
|
|
157
179
|
for (let j = i + 1; j < writable.length; j++) {
|
|
180
|
+
if ((writable[i].constraints.dup || writable[j].constraints.dup) && inPlace(writable[i]) && inPlace(writable[j]))
|
|
181
|
+
continue;
|
|
158
182
|
const present = [writable[i], writable[j]]
|
|
159
183
|
.filter((a) => a.optional)
|
|
160
184
|
.map((a) => `a_${a.name} && `)
|
|
@@ -331,6 +355,10 @@ export class FunctionEmitter extends StatementEmitter {
|
|
|
331
355
|
for (const name of this.pendingInit) {
|
|
332
356
|
const k = this.accounts.get(name).kind;
|
|
333
357
|
const a = this.accounts.get(name);
|
|
358
|
+
if (k.kind === "migrate") {
|
|
359
|
+
this.reporter.error(a.node, `Migrate account "${name}" is never migrated; call ${name}.migrate(payer, value) (Anchor fails such an instruction with AccountNotMigrated)`);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
334
362
|
const how = k.kind === "token" && a.constraints.ata
|
|
335
363
|
? `${name}.${k.init === "create" ? "create" : "createIfNeeded"}(payer)`
|
|
336
364
|
: k.kind === "token"
|
|
@@ -277,6 +277,24 @@ export class StorageEmitter extends EmitterBase {
|
|
|
277
277
|
}
|
|
278
278
|
if (ts.isPropertyAccessExpression(node)) {
|
|
279
279
|
const field = node.name.text;
|
|
280
|
+
// Migrate<From, To>: acct.from (the old data, read-only, until migrate()) and acct.to (after it).
|
|
281
|
+
if (ts.isIdentifier(node.expression)) {
|
|
282
|
+
const m = this.accounts.get(node.expression.text);
|
|
283
|
+
if (m?.kind.kind === "migrate") {
|
|
284
|
+
const migrated = this.created.has(m.name);
|
|
285
|
+
if (field === "from") {
|
|
286
|
+
if (migrated)
|
|
287
|
+
this.fail(node, `${m.name}.from is gone after ${m.name}.migrate(…): read what you need from it before`);
|
|
288
|
+
return { lv: `(*df_${m.name})`, type: { kind: "struct", name: m.kind.from.name }, writable: false, account: m, path: ["from"] };
|
|
289
|
+
}
|
|
290
|
+
if (field === "to") {
|
|
291
|
+
if (!migrated)
|
|
292
|
+
this.fail(node, `${m.name}.to exists after ${m.name}.migrate(payer, value)`);
|
|
293
|
+
return { lv: `(*dt_${m.name})`, type: { kind: "struct", name: m.kind.to.name }, writable: true, account: m, path: ["to"] };
|
|
294
|
+
}
|
|
295
|
+
this.fail(node.name, `a Migrate account has .from (before migrate), .to (after), key, lamports and migrate(payer, value)`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
280
298
|
// acct.field on a struct account
|
|
281
299
|
if (ts.isIdentifier(node.expression)) {
|
|
282
300
|
const a = this.accounts.get(node.expression.text);
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -63,6 +63,10 @@ export type ValueType = ScalarType | {
|
|
|
63
63
|
| {
|
|
64
64
|
kind: "remaining";
|
|
65
65
|
}
|
|
66
|
+
/** One instruction of the transaction (InstructionsSysvar.get(i)): a `TsIx` view into the sysvar. */
|
|
67
|
+
| {
|
|
68
|
+
kind: "txIx";
|
|
69
|
+
}
|
|
66
70
|
/** A struct loaded from a remaining account with loadAccount / loadAccountMut. */
|
|
67
71
|
| {
|
|
68
72
|
kind: "structRef";
|
|
@@ -190,6 +194,12 @@ export type ParamKind = {
|
|
|
190
194
|
kind: "zeroed";
|
|
191
195
|
struct: StructDef;
|
|
192
196
|
}
|
|
197
|
+
/** Anchor's Migration<From, To>: an existing `from` account rewritten as `to` by `acct.migrate(payer, value)`. */
|
|
198
|
+
| {
|
|
199
|
+
kind: "migrate";
|
|
200
|
+
from: StructDef;
|
|
201
|
+
to: StructDef;
|
|
202
|
+
}
|
|
193
203
|
/** `init`: an associated token account this instruction creates (`Ata<Init<TokenAccount>, …>`, create) or creates when
|
|
194
204
|
* missing (`Ata<InitIfNeeded<TokenAccount>, …>`, createIfNeeded); it is a TokenAccount once that call has run. */
|
|
195
205
|
| {
|
|
@@ -263,6 +273,8 @@ export interface Constraints {
|
|
|
263
273
|
};
|
|
264
274
|
/** Executable<A>: the account must be an executable program. */
|
|
265
275
|
executable?: boolean;
|
|
276
|
+
/** Dup<A> (Anchor's `dup`): may be the same account as another writable parameter (see function.ts). */
|
|
277
|
+
dup?: boolean;
|
|
266
278
|
/** Close this account into that parameter when the instruction succeeds. */
|
|
267
279
|
closeTo?: string;
|
|
268
280
|
/** Address<A, "base58">: the account must be exactly this address. */
|
package/dist/compiler/model.js
CHANGED
|
@@ -20,6 +20,7 @@ export function isWritable(k) {
|
|
|
20
20
|
case "init":
|
|
21
21
|
case "zeroed":
|
|
22
22
|
case "initIfNeeded":
|
|
23
|
+
case "migrate":
|
|
23
24
|
return true;
|
|
24
25
|
case "signer":
|
|
25
26
|
case "info":
|
|
@@ -67,6 +68,8 @@ export function typeName(t) {
|
|
|
67
68
|
return "AccountInfo";
|
|
68
69
|
case "remaining":
|
|
69
70
|
return "RemainingAccounts";
|
|
71
|
+
case "txIx":
|
|
72
|
+
return "TransactionInstruction";
|
|
70
73
|
case "structRef":
|
|
71
74
|
return t.struct;
|
|
72
75
|
}
|
|
@@ -184,9 +184,9 @@ Both implementations refuse the hostile calls the comparison makes:
|
|
|
184
184
|
|
|
185
185
|
TypeSOL checks each account's constraints in Anchor's order, so a refused transaction usually gets the same error
|
|
186
186
|
code (see [which error a transaction gets](errors.md#which-error-a-transaction-gets)). The second call differs on
|
|
187
|
-
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters
|
|
188
|
-
where Anchor only compares program and token accounts and fails later, on the maker's
|
|
189
|
-
matches on this code needs to accept either.
|
|
187
|
+
purpose: taker and maker are then the same writable account, which TypeSOL refuses for any two writable parameters
|
|
188
|
+
unless one is marked `Dup<…>`, where Anchor only compares program and token accounts and fails later, on the maker's
|
|
189
|
+
token account. A client that matches on this code needs to accept either.
|
|
190
190
|
|
|
191
191
|
## Cost
|
|
192
192
|
|
package/docs/errors.md
CHANGED
|
@@ -70,5 +70,5 @@ Where TypeSOL's order differs, only a transaction that breaks two of these check
|
|
|
70
70
|
- Too few accounts fails with 3005 before any account is checked; Anchor fails on the first account it cannot find.
|
|
71
71
|
- Accounts the instruction creates (`Init<…>`, `create(…)`) are created in the body, after every other check, where
|
|
72
72
|
Anchor creates them before checking the other accounts.
|
|
73
|
-
- TypeSOL refuses any two writable parameters naming one account (2040)
|
|
73
|
+
- TypeSOL refuses any two writable parameters naming one account (2040) unless one is `Dup<…>`, where Anchor only compares program and token
|
|
74
74
|
accounts: a signer passed as two writable parameters fails with 2040 here and later, or not at all, under Anchor.
|
package/docs/language.md
CHANGED
|
@@ -38,7 +38,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
38
38
|
| Option of structs | `home: Option<Point>` in account data — `=== null`, `p.home.x` after narrowing, `p.home = null` / `{ x, y }` |
|
|
39
39
|
| Strings at runtime | `"hello, " + name`, `` `#${n}` ``, `` `owner ${acct.owner}` `` (a Pubkey or Hash as base58) — built on the 32 KiB heap (5110 when full): a template is joined in one allocation, text known at compile time (literals, string and Pubkey constants) is folded in, and `s += …` grows the newest heap string in place |
|
|
40
40
|
| `switch` | on integers (128-bit ones too), enums and data-enum `.kind`, with JS `break` / fallthrough; case labels are literals, constants or enum members |
|
|
41
|
-
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
41
|
+
| Hashing & signatures | `sha256(...)`, `keccak256(...)` → `Hash`; `findProgramAddress(seeds, program)` → the canonical PDA of any program (Rust `Pubkey::find_program_address(..).0`, e.g. a Bubblegum asset id); `merkleVerify(leaf, proof: Vec<Hash>, root)` (sorted-pair keccak, OpenZeppelin convention; inner nodes and leaves are hashed alike, so hash leaves as OpenZeppelin does, `keccak256(keccak256(data))`, and never take the leaf itself from the arguments: an inner node would pass as a leaf); `verifyEd25519(instructionsSysvar, signer, message)` checks an Ed25519 precompile instruction in the same transaction; `stackHeight()` is 1 when the transaction calls the program directly, more under a CPI (`require(stackHeight() === 1n, …)` refuses CPI callers); `verifySecp256k1(keccak256(...message) | ethMessageHash(message), recoveryId, r, s, ethAddress)` (the hash must be computed in the program; a passed-in hash could be forged) |
|
|
42
42
|
| Oracles | `pythPrice(update: PythPriceUpdate, feedId, maxAgeSeconds, exponent?)` (fully verified, feed-matched, fresh; with `exponent`, a feed at another scale is refused); raw `update.price / conf / exponent / publishTime` are unchecked (any feed, partially verified or stale: check `feedId`, `fullyVerified` and the age yourself, or use `pythPrice`); `switchboardValue(feed: SwitchboardFeed, expectedFeed, maxStaleSlots, decimals, minSamples?)` (the feed must be the expected one: anyone can create feeds; `minSamples`, default 1, the fewest oracle samples accepted) (mainnet program unless `SwitchboardFeed<"devnet">`) |
|
|
43
43
|
| Multiple files | `import { Side, min } from "./math"` — structs, enums, constants and helpers can live in other files; only the entry file's exported functions are instructions; names are program-wide, so imports from TypeSOL files cannot be renamed; error codes number imported files first |
|
|
44
44
|
| Loops | `for (let i = 0n; i < n; i++)`, `while`, `do … while`, `for (const x of list)` over a `Vec` or `FixedArray` (each `x` is what `list.get(i)` gives: a value, or a reference into account data for structs; the length is read again every step, as in JS), `break`, `continue` — bounded by the compute budget. Remaining accounts are iterated by index |
|
|
@@ -76,6 +76,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
76
76
|
| `A \| null` / `Optional<A>` | optional account (Anchor `Option<Account>`): the client passes this program's id to leave it out; its checks and constraints run only when present; `x !== null` before use, or `x!` / `x!.field` to assert it was passed (5115 otherwise); `optional` in the IDL |
|
|
77
77
|
| `ProgramData` | this program's ProgramData (address derived from the program id, upgradeable-loader owner and state checked); `upgradeAuthority`, `deploySlot` — gate admin setup on `programData.upgradeAuthority === admin.key` |
|
|
78
78
|
| `EventAuthority`, `SelfProgram` | Anchor's `["__event_authority"]` PDA, and this program's own address, for `emitCpi` |
|
|
79
|
+
| `Migrate<From, To>` | Anchor 1.0's `Migration`: an existing `From` account (owner, discriminator, size) rewritten as `To` by `acct.migrate(payer, value)`, a top-level statement the instruction must reach. `acct.from` (read-only) before it, `acct.to` after; the value is built in full before the rewrite, so it may read `acct.from`. Resized to `To` (the payer tops up rent when it grows, at most 10 KiB; shrinking keeps the lamports; growing needs `SystemProgram`). Fixed-layout structs without growable Vecs; no constraints yet; the account cannot be passed to a helper (use `acct.to` fields in the instruction). An account already migrated fails with 3002. Keep the old struct under a new name with its discriminator: `/** @discriminator account:Counter */ interface CounterV1 { … }` |
|
|
79
80
|
| `Zeroed<T>` | large accounts (up to 10 MiB) the client allocated with System `CreateAccount`: owned by this program, big enough, rent-exempt, discriminator still zero — then initialized; like Anchor's zero-copy `zero` |
|
|
80
81
|
| `Init<Mint>`, `InitIfNeeded<Mint>` | Anchor `init` / `init_if_needed` with `mint::decimals` / `mint::authority` / `mint::freeze_authority`: `const bump = mint.create(payer, ["mint", payer.key], 6n, authority, freezeAuthority \| null)` creates the mint at that PDA of this program, or with `null` seeds as a new keypair account that signs (3010 if it did not), under the instruction's token program (its only token program parameter, or one passed as the last argument of `create` / `createIfNeeded`; plus `SystemProgram`). Token-2022 extensions (Anchor's `extensions::…`) go after the freeze authority: `mint.create(payer, null, 6n, authority, null, { metadataPointer: { authority, metadataAddress: mint.key }, transferHook: { authority, programId }, closeAuthority, permanentDelegate, nonTransferable: true, defaultAccountState: true, interestBearing: { rateAuthority, rate }, transferFee: { configAuthority, withdrawAuthority, basisPoints, maximumFee }, groupPointer, groupMemberPointer })` sizes the mint for them and initializes each before InitializeMint2. With `createIfNeeded` an existing mint must carry groupPointer / groupMemberPointer / metadataPointer / closeAuthority / permanentDelegate / transferHook with those keys (Anchor's 2024–2039); extensions not listed are not looked at, so an existing mint may carry others (a permanent delegate, say). `createIfNeeded(…)` returns whether it created it; an existing one must be that PDA and have those decimals and authorities (2018 / 2016 / 2017) |
|
|
81
82
|
| `Init<TokenAccount>`, `InitIfNeeded<TokenAccount>` (no `Ata<…>`) | Anchor `init` / `init_if_needed` with `token::mint` / `token::authority`: `vault.create(payer, ["vault", mint.key], mint, authority)` (the authority may be `vault.key` itself) creates a token account at that PDA (or a keypair account with `null`), sized for the mint's extensions under Token-2022. `createIfNeeded` checks an existing one has that mint and owner (2014 / 2015), not its delegate or close authority (as Anchor) |
|
|
@@ -86,6 +87,7 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
86
87
|
| `AssociatedTokenProgram` | `create` / `createIdempotent` CPIs; `associatedTokenAddress(owner, mint, tokenProgram)` |
|
|
87
88
|
| `TokenMetadataProgram` | Metaplex `createV3(...)` (CreateMetadataAccountV3), optionally with `{ creators: [{ address, verified, share }], collection: mint }` after `isMutable` (1–5 creators, shares adding up to 100; only the update authority, signing, can be `verified: true`; the collection is written unverified; `isCollection: true` makes the NFT a sized collection); `createMasterEditionV3(edition, mint, updateAuthority, mintAuthority, payer, metadata, tokenProgram, systemProgram, maxSupply \| null)`; `verifyCollection(authority, metadata, collectionMint, collectionMetadata, collectionMasterEdition, systemProgram, instructionsSysvar)` (Verify with CollectionV1); `metadataAddress(mint)`, `masterEditionAddress(mint)` |
|
|
88
89
|
| `InstructionsSysvar`, `PythPriceUpdate`, `SwitchboardFeed` | exact address / owner program + discriminator |
|
|
90
|
+
| `InstructionsSysvar` (introspection) | `ixs.length`, `ixs.currentIndex()` (the top-level instruction being executed), `ixs.get(i)` → a `TransactionInstruction`: `programId`, `accountsLength`, `dataLength`, `accountKey(j)` / `accountIsSigner(j)` / `accountIsWritable(j)`, `readU8/U16/U32/U64/readPubkey(offset)` (little-endian), `is(program, "name")` (that program's instruction by its default Anchor discriminator, sha256("global:<name>"); an instruction with its own `@discriminator` is matched with `programId` and `readU64(0)` instead). Every read is bounds-checked (5104). It is the transaction author's data: require things of the transaction with it (a repay instruction after a flash loan), never treat it as proof |
|
|
89
91
|
| `Program<"Address…">` | exact program address + executable |
|
|
90
92
|
| `UncheckedProgram` | executable only — check `.key` yourself |
|
|
91
93
|
| `AccountInfo` / `Mut<AccountInfo>` | nothing (writable for `Mut`); `owner`, `executable` |
|
|
@@ -94,7 +96,10 @@ The full reference. For a tour, start with the [README](../README.md).
|
|
|
94
96
|
Every account exposes `key`, `lamports`, `dataLen`, `isSigner`, `isWritable`. At most 32 accounts per instruction (more is refused, 5113).
|
|
95
97
|
|
|
96
98
|
Two writable parameters can never be the same account: aliasing is refused with error 2040 (also when a remaining
|
|
97
|
-
account written through `.lamports` or `loadAccountMut` is another account of the instruction).
|
|
99
|
+
account written through `.lamports` or `loadAccountMut` is another account of the instruction). `Dup<Mut<T>>`
|
|
100
|
+
(Anchor's `dup`) lifts that for one parameter when both accounts are read and written in place (not created, closed,
|
|
101
|
+
a close destination or `@layout borsh`): a write through either is seen through the other, so read what you need
|
|
102
|
+
before writing. A remaining account
|
|
98
103
|
can be loaded with `loadAccountMut` once per instruction: loading the same one again (`rest.get(i)` and `rest.get(j)`
|
|
99
104
|
with `i == j`) is refused with 2040 too, since the two views would alias.
|
|
100
105
|
|
|
@@ -46,6 +46,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
46
46
|
| `Option<Account<'info, T>>` | `Mut<T> \| null` (the client passes the program id to leave it out) |
|
|
47
47
|
| `UncheckedAccount` / `AccountInfo` | `AccountInfo` / `Mut<AccountInfo>`: check what you rely on yourself |
|
|
48
48
|
| `SystemAccount<'info>` | `SystemAccount` / `Mut<SystemAccount>` |
|
|
49
|
+
| `Migration<'info, From, To>` | `Migrate<From, To>`, then `acct.migrate(payer, value)`; the old struct keeps its on-chain discriminator with `/** @discriminator account:<OldName> */` |
|
|
50
|
+
| `#[account(discriminator = …)]` | `/** @discriminator <preimage or 0x8 bytes> */` on the struct |
|
|
49
51
|
| `AccountLoader<T>` with `#[account(zero)]` | `Zeroed<T>` (TypeSOL reads every account in place: no copy to avoid) |
|
|
50
52
|
| `Program<'info, System>` | `SystemProgram` |
|
|
51
53
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
@@ -57,6 +59,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
57
59
|
| `#[account(init, payer = p, seeds = […], bump, token::mint = m, token::authority = a)]` | `Init<TokenAccount>`, then `vault.create(p, […], m, a.key)` (`InitIfNeeded<TokenAccount>` + `createIfNeeded`) |
|
|
58
60
|
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">`; `Ata<Init<TokenAccount>, "a", "m">` creates it |
|
|
59
61
|
| `ctx.remaining_accounts` | `remainingAccounts()` |
|
|
62
|
+
| `#[account(mut, dup)]` | `Dup<Mut<T>>` (in-place accounts only) |
|
|
63
|
+
| `sysvar::instructions` (`load_current_index_checked`, `load_instruction_at_checked`), `get_stack_height()` | `InstructionsSysvar`: `currentIndex()`, `get(i)`, `.length`; `stackHeight()` |
|
|
60
64
|
| `ctx.bumps.vault` | the value `create` returns, or `acct.pdaBump` |
|
|
61
65
|
|
|
62
66
|
## Account data
|
package/docs/security-model.md
CHANGED
|
@@ -31,7 +31,8 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
31
31
|
initialized account of that kind (3007 / 3003); program parameters at their fixed address and executable
|
|
32
32
|
(3008 / 3009); `SystemAccount` owned by the System Program (3011); `Zeroed<T>` owned, all-zero discriminator and
|
|
33
33
|
rent-exempt.
|
|
34
|
-
3. **No aliasing.** Two writable parameters are never the same account (2040), whatever slots the transaction uses
|
|
34
|
+
3. **No aliasing.** Two writable parameters are never the same account (2040), whatever slots the transaction uses,
|
|
35
|
+
unless the program marks one `Dup<…>`, and then only for accounts read and written in place (see below).
|
|
35
36
|
4. **Arguments.** Every argument is length-checked before it is read; strings are valid UTF-8; booleans, enums and
|
|
36
37
|
options have valid tags (102).
|
|
37
38
|
5. **Constraints.** `Seeds` (2006, with the stored or the canonical bump), `Ata` (2015 / 2009 / 2014), `HasOne`
|
|
@@ -50,7 +51,9 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
50
51
|
written before it (compile error). Creation refuses an account that already exists (3000), and an address someone
|
|
51
52
|
pre-funded still gets created. `createIfNeeded` on an existing account re-derives the canonical PDA and checks
|
|
52
53
|
owner, discriminator and size (token accounts: mint and owner; mints: decimals, authorities and the extensions it
|
|
53
|
-
names).
|
|
54
|
+
names). A `Migrate<From, To>` account is checked as an existing `From`; `migrate(…)` is a top-level statement every
|
|
55
|
+
path reaches, builds the new value before rewriting the bytes, and nothing reads the old data after it (compile
|
|
56
|
+
errors).
|
|
54
57
|
9. **Writes through references.** A place is never written after the code that computes its value may have moved it:
|
|
55
58
|
replacing or resizing what a live reference points into is a compile error, as is writing a Vec element with a
|
|
56
59
|
right side that may resize the Vec.
|
|
@@ -78,6 +81,10 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
78
81
|
- **`AccountInfo` / `UncheckedProgram`.** Unchecked by design, as in Anchor: check what you rely on.
|
|
79
82
|
- **`requirePda(account, seeds, bump)`.** Any valid bump passes (Anchor's `bump = expr`). Use a stored bump, never one
|
|
80
83
|
from the arguments.
|
|
84
|
+
- **`Dup<…>`.** A parameter marked `Dup` may be the same account as another writable one. Reads see every write
|
|
85
|
+
(both are the same bytes), so a value read before a write through the other slot is stale: read, then write.
|
|
86
|
+
- **Instruction introspection.** The Instructions sysvar holds what the transaction's author put there. Require things
|
|
87
|
+
of the transaction with it (that a repay instruction follows), never treat it as proof of who did what.
|
|
81
88
|
- **Oracles.** `pythPrice` checks verification, feed and age (pass the exponent you scale by); the raw `update.*`
|
|
82
89
|
fields are unchecked. `switchboardValue` needs the expected feed; set `minSamples`.
|
|
83
90
|
- **`merkleVerify`.** Leaves and inner nodes hash alike: build leaves as `keccak256(keccak256(data))` in the program.
|
|
@@ -87,7 +94,8 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
87
94
|
|
|
88
95
|
## Where TypeSOL is stricter than Anchor
|
|
89
96
|
|
|
90
|
-
- Any two writable parameters naming one account are refused (Anchor compares only program
|
|
97
|
+
- Any two writable parameters naming one account are refused unless one is `Dup<…>` (Anchor compares only program
|
|
98
|
+
and token accounts, and `dup` lifts that).
|
|
91
99
|
- Arithmetic overflow returns an error code (5100) in every build; Anchor aborts the program, and only when
|
|
92
100
|
overflow checks are enabled.
|
|
93
101
|
|
package/lib/typesol.d.ts
CHANGED
|
@@ -186,10 +186,43 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
|
|
|
186
186
|
*/
|
|
187
187
|
export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
|
|
188
188
|
|
|
189
|
-
/**
|
|
189
|
+
/**
|
|
190
|
+
* The Instructions sysvar (Sysvar1nstructions…): every instruction of the transaction being executed. Its address is
|
|
191
|
+
* checked. What it holds is the transaction author's: use it to require something of the transaction (a repay
|
|
192
|
+
* instruction after a flash loan, a fee instruction before), never as proof of anything else.
|
|
193
|
+
*/
|
|
190
194
|
export interface InstructionsSysvar extends AccountMeta {
|
|
191
195
|
readonly __instructions_sysvar: void;
|
|
196
|
+
/** How many instructions the transaction has. */
|
|
197
|
+
readonly length: u64;
|
|
198
|
+
/** The index of the top-level instruction being executed (this one, or the one that CPI'd into this program). */
|
|
199
|
+
currentIndex(): u64;
|
|
200
|
+
/** The transaction's instruction at `index` (5104 when there is none). */
|
|
201
|
+
get(index: u64): TransactionInstruction;
|
|
192
202
|
}
|
|
203
|
+
/** One instruction of the transaction, read from the Instructions sysvar. Reads past its end fail with 5104. */
|
|
204
|
+
export interface TransactionInstruction {
|
|
205
|
+
readonly __transaction_instruction: void;
|
|
206
|
+
readonly programId: Pubkey;
|
|
207
|
+
readonly accountsLength: u64;
|
|
208
|
+
readonly dataLength: u64;
|
|
209
|
+
accountKey(i: u64): Pubkey;
|
|
210
|
+
accountIsSigner(i: u64): boolean;
|
|
211
|
+
accountIsWritable(i: u64): boolean;
|
|
212
|
+
/** Little-endian integers and keys from the instruction data. */
|
|
213
|
+
readU8(offset: u64): u8;
|
|
214
|
+
readU16(offset: u64): u16;
|
|
215
|
+
readU32(offset: u64): u32;
|
|
216
|
+
readU64(offset: u64): u64;
|
|
217
|
+
readPubkey(offset: u64): Pubkey;
|
|
218
|
+
/** `program`'s instruction `name` (snake_case or camelCase): its program id and 8-byte Anchor discriminator match. */
|
|
219
|
+
is(program: Pubkey, name: string): boolean;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* How deep in cross-program calls this program runs: 1 when the transaction calls it directly, more when another
|
|
223
|
+
* program invoked it. `require(stackHeight() === 1n, …)` refuses CPI callers.
|
|
224
|
+
*/
|
|
225
|
+
export declare function stackHeight(): u64;
|
|
193
226
|
|
|
194
227
|
/**
|
|
195
228
|
* True when this transaction contains an Ed25519 precompile instruction that verified `message`
|
|
@@ -597,6 +630,27 @@ export type TokenOf<A, MintParam extends string, AuthorityParam extends string =
|
|
|
597
630
|
export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
598
631
|
readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
|
|
599
632
|
};
|
|
633
|
+
/**
|
|
634
|
+
* Anchor's `dup`: this writable account may be the same account as another writable parameter (TypeSOL refuses that
|
|
635
|
+
* otherwise, 2040). Both are read and written in place, so a write through one is seen through the other: read
|
|
636
|
+
* what you need before writing. Not for accounts that are created, closed, a close destination or `@layout borsh`.
|
|
637
|
+
*/
|
|
638
|
+
export type Dup<A> = A & { readonly __dup?: true };
|
|
639
|
+
/**
|
|
640
|
+
* Anchor 1.0's `Migration<From, To>`: an existing `From` account (owner, discriminator and size checked) that this
|
|
641
|
+
* instruction rewrites as `To` with `acct.migrate(payer, value)`, a top-level statement every path must reach. Read
|
|
642
|
+
* the old data through `acct.from` before migrating (the new value may use it: it is built in full before the account
|
|
643
|
+
* is rewritten), and the new data through `acct.to` after. The account is resized to `To`: the payer tops up the rent
|
|
644
|
+
* when it grows (at most 10 KiB), and shrinking keeps the lamports. `From` usually keeps the old type's discriminator
|
|
645
|
+
* under a new name: `/** @discriminator account:Counter *\/ interface CounterV1 { … }`. An account already migrated
|
|
646
|
+
* fails with 3002, as under Anchor.
|
|
647
|
+
*/
|
|
648
|
+
export interface Migrate<From, To> extends AccountMeta {
|
|
649
|
+
readonly __migrate: [From, To];
|
|
650
|
+
readonly from: Readonly<From>;
|
|
651
|
+
readonly to: To;
|
|
652
|
+
migrate(payer: Mut<Signer>, value: To): void;
|
|
653
|
+
}
|
|
600
654
|
export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
|
|
601
655
|
readonly __seeds?: [Names, Bump, ProgramId];
|
|
602
656
|
/** The PDA's bump (Anchor `ctx.bumps.<account>`): the stored bump field when one is named, else the canonical bump found. */
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -1121,6 +1121,81 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
|
|
|
1121
1121
|
return ts_mem_eq(node, root, 32);
|
|
1122
1122
|
}
|
|
1123
1123
|
|
|
1124
|
+
/*
|
|
1125
|
+
* The Instructions sysvar: u16 count, u16 offsets[count], then per instruction u16 num_accounts,
|
|
1126
|
+
* num_accounts * (u8 flags: bit 0 signer, bit 1 writable; 32-byte key), 32-byte program id, u16 data_len, data; the
|
|
1127
|
+
* last two bytes of the account are the index of the instruction being executed. Everything here is the transaction
|
|
1128
|
+
* author's data: bounds-checked, never trusted for anything but what it says.
|
|
1129
|
+
*/
|
|
1130
|
+
typedef struct {
|
|
1131
|
+
const uint8_t *accounts; /* num_accounts * 33 bytes */
|
|
1132
|
+
const SolPubkey *program_id;
|
|
1133
|
+
const uint8_t *data;
|
|
1134
|
+
uint16_t num_accounts;
|
|
1135
|
+
uint16_t data_len;
|
|
1136
|
+
} TsIx;
|
|
1137
|
+
|
|
1138
|
+
/* Instruction `index` of the transaction; false when out of range or malformed. */
|
|
1139
|
+
static inline bool ts_ix_at(const TsAccount *ixs, uint64_t index, TsIx *out) {
|
|
1140
|
+
const uint8_t *d = ixs->data;
|
|
1141
|
+
uint64_t len = ixs->data_len;
|
|
1142
|
+
if (len < 2) return false;
|
|
1143
|
+
uint16_t count;
|
|
1144
|
+
ts_memcpy(&count, d, 2);
|
|
1145
|
+
if (index >= count || 2 + (uint64_t)count * 2 > len) return false;
|
|
1146
|
+
uint16_t at;
|
|
1147
|
+
ts_memcpy(&at, d + 2 + index * 2, 2);
|
|
1148
|
+
uint64_t p = at;
|
|
1149
|
+
if (p + 2 > len) return false;
|
|
1150
|
+
ts_memcpy(&out->num_accounts, d + p, 2);
|
|
1151
|
+
p += 2;
|
|
1152
|
+
out->accounts = d + p;
|
|
1153
|
+
p += (uint64_t)out->num_accounts * 33;
|
|
1154
|
+
if (p + 34 > len) return false;
|
|
1155
|
+
out->program_id = (const SolPubkey *)(d + p);
|
|
1156
|
+
p += 32;
|
|
1157
|
+
ts_memcpy(&out->data_len, d + p, 2);
|
|
1158
|
+
p += 2;
|
|
1159
|
+
if (p + out->data_len > len) return false;
|
|
1160
|
+
out->data = d + p;
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
1163
|
+
static inline uint64_t ts_ix_load(const TsAccount *ixs, uint64_t index, TsIx *out) {
|
|
1164
|
+
if (!ts_ix_at(ixs, index, out)) TS_FAIL(TS_ERR_INDEX, "TypeSOL: no such instruction in the transaction");
|
|
1165
|
+
return SUCCESS;
|
|
1166
|
+
}
|
|
1167
|
+
static inline uint64_t ts_ix_count(const TsAccount *ixs, uint64_t *out) {
|
|
1168
|
+
if (ixs->data_len < 2) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
|
|
1169
|
+
uint16_t n;
|
|
1170
|
+
ts_memcpy(&n, ixs->data, 2);
|
|
1171
|
+
*out = n;
|
|
1172
|
+
return SUCCESS;
|
|
1173
|
+
}
|
|
1174
|
+
static inline uint64_t ts_ix_current(const TsAccount *ixs, uint64_t *out) {
|
|
1175
|
+
if (ixs->data_len < 4) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
|
|
1176
|
+
uint16_t i;
|
|
1177
|
+
ts_memcpy(&i, ixs->data + ixs->data_len - 2, 2);
|
|
1178
|
+
*out = i;
|
|
1179
|
+
return SUCCESS;
|
|
1180
|
+
}
|
|
1181
|
+
static inline uint64_t ts_ix_account(const TsIx *ix, uint64_t i, const uint8_t **out) {
|
|
1182
|
+
if (i >= ix->num_accounts) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the instruction has no account at that index");
|
|
1183
|
+
*out = ix->accounts + i * 33;
|
|
1184
|
+
return SUCCESS;
|
|
1185
|
+
}
|
|
1186
|
+
/* `n` bytes of the instruction's data at `off` (little-endian integers, keys). */
|
|
1187
|
+
static inline uint64_t ts_ix_read(const TsIx *ix, uint64_t off, uint64_t n, void *out) {
|
|
1188
|
+
if (off > ix->data_len || n > ix->data_len - off) TS_FAIL(TS_ERR_INDEX, "TypeSOL: read past the end of the instruction's data");
|
|
1189
|
+
ts_memcpy(out, ix->data + off, n);
|
|
1190
|
+
return SUCCESS;
|
|
1191
|
+
}
|
|
1192
|
+
/* Is it `program`'s instruction with this 8-byte discriminator (Anchor's sha256("global:<name>")[..8])? */
|
|
1193
|
+
static inline bool ts_ix_is(const TsIx *ix, const SolPubkey *program, const uint8_t disc[8]) {
|
|
1194
|
+
return ts_key_eq(ix->program_id, program) && ix->data_len >= 8 && ts_mem_eq(ix->data, disc, 8);
|
|
1195
|
+
}
|
|
1196
|
+
/* How deep in CPIs the program runs: 1 when the transaction calls it directly. */
|
|
1197
|
+
extern uint64_t sol_get_stack_height(void);
|
|
1198
|
+
|
|
1124
1199
|
/*
|
|
1125
1200
|
* Did an Ed25519 precompile instruction in this transaction verify `message` signed by `signer`?
|
|
1126
1201
|
* Reads the Instructions sysvar (data: u16 count, u16 offsets[count], then per instruction
|
|
@@ -1129,29 +1204,15 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
|
|
|
1129
1204
|
* message, with offsets pointing into the precompile's own data (instruction index 0xFFFF).
|
|
1130
1205
|
*/
|
|
1131
1206
|
static inline bool ts_ed25519_verified(const TsAccount *ixs, const SolPubkey *signer, TS_Str message) {
|
|
1132
|
-
|
|
1133
|
-
uint64_t len = ixs->data_len;
|
|
1134
|
-
if (len < 2) return false;
|
|
1207
|
+
if (ixs->data_len < 2) return false;
|
|
1135
1208
|
uint16_t count;
|
|
1136
|
-
ts_memcpy(&count,
|
|
1137
|
-
if (2 + (uint64_t)count * 2 > len) return false;
|
|
1209
|
+
ts_memcpy(&count, ixs->data, 2);
|
|
1138
1210
|
for (uint16_t i = 0; i < count; i++) {
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
ts_memcpy(&nacc, d + p, 2);
|
|
1145
|
-
p += 2 + (uint64_t)nacc * 33;
|
|
1146
|
-
if (p + 34 > len) return false;
|
|
1147
|
-
bool is_ed = ts_mem_eq(d + p, &TS_ED25519_PROGRAM_ID, 32);
|
|
1148
|
-
p += 32;
|
|
1149
|
-
uint16_t dlen;
|
|
1150
|
-
ts_memcpy(&dlen, d + p, 2);
|
|
1151
|
-
p += 2;
|
|
1152
|
-
if (p + dlen > len) return false;
|
|
1153
|
-
if (!is_ed || dlen < 2) continue;
|
|
1154
|
-
const uint8_t *ix = d + p;
|
|
1211
|
+
TsIx t;
|
|
1212
|
+
if (!ts_ix_at(ixs, i, &t)) return false;
|
|
1213
|
+
if (!ts_key_eq(t.program_id, &TS_ED25519_PROGRAM_ID) || t.data_len < 2) continue;
|
|
1214
|
+
uint16_t dlen = t.data_len;
|
|
1215
|
+
const uint8_t *ix = t.data;
|
|
1155
1216
|
uint8_t nsig = ix[0];
|
|
1156
1217
|
for (uint8_t k = 0; k < nsig; k++) {
|
|
1157
1218
|
uint64_t o = 2 + (uint64_t)k * 14;
|