@typesol/cli 0.26.0 → 0.28.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 +39 -0
- package/README.md +3 -1
- package/dist/compiler/analyze.d.ts +2 -0
- package/dist/compiler/analyze.js +93 -12
- package/dist/compiler/emit/base.d.ts +3 -0
- package/dist/compiler/emit/base.js +4 -1
- package/dist/compiler/emit/calls.d.ts +16 -1
- package/dist/compiler/emit/calls.js +131 -1
- package/dist/compiler/emit/cpi.d.ts +9 -0
- package/dist/compiler/emit/cpi.js +63 -0
- package/dist/compiler/emit/expressions.js +18 -0
- package/dist/compiler/emit/function.js +41 -5
- package/dist/compiler/emit/shared.js +2 -0
- package/dist/compiler/emit/storage.js +40 -1
- package/dist/compiler/idl.js +1 -1
- package/dist/compiler/model.d.ts +14 -0
- package/dist/compiler/model.js +3 -0
- package/docs/architecture.md +2 -1
- package/docs/audit.md +1 -0
- package/docs/case-study-escrow.md +3 -3
- package/docs/case-study-multisig.md +167 -0
- package/docs/errors.md +4 -3
- package/docs/language.md +26 -6
- package/docs/migrate-from-anchor.md +9 -0
- package/docs/security-model.md +15 -3
- package/lib/typesol.d.ts +92 -5
- package/package.json +1 -1
- package/runtime/typesol_rt.h +90 -23
|
@@ -46,7 +46,12 @@ 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) |
|
|
52
|
+
| `Account<T>` with `#[account(zero)]` (Borsh) | `Zeroed<T>` of a `/** @layout borsh */` struct |
|
|
53
|
+
| `#[account(signer)]` on a non-`Signer` account | `Signed<A>`, e.g. `Signed<Zeroed<Multisig>>` |
|
|
54
|
+
| `seeds = [multisig.key().as_ref()], bump = multisig.nonce` | `Seeds<A, ["@multisig"], "@multisig.nonce">` |
|
|
50
55
|
| `Program<'info, System>` | `SystemProgram` |
|
|
51
56
|
| `Program<'info, Token>` / `Token2022` / `Interface<'info, TokenInterface>` | `TokenProgram` / `Token2022Program` / `TokenInterface` |
|
|
52
57
|
| `Account<'info, TokenAccount>` / `Account<'info, Mint>` (`InterfaceAccount`) | `TokenAccount` / `Mint` (`Mut<…>` when written) |
|
|
@@ -57,6 +62,8 @@ and error codes. Its notes (on stderr) list what an existing Anchor client would
|
|
|
57
62
|
| `#[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
63
|
| `associated_token::mint = m, associated_token::authority = a` | `Ata<TokenAccount, "a", "m">`; `Ata<Init<TokenAccount>, "a", "m">` creates it |
|
|
59
64
|
| `ctx.remaining_accounts` | `remainingAccounts()` |
|
|
65
|
+
| `#[account(mut, dup)]` | `Dup<Mut<T>>` (in-place accounts only) |
|
|
66
|
+
| `sysvar::instructions` (`load_current_index_checked`, `load_instruction_at_checked`), `get_stack_height()` | `InstructionsSysvar`: `currentIndex()`, `get(i)`, `.length`; `stackHeight()` |
|
|
60
67
|
| `ctx.bumps.vault` | the value `create` returns, or `acct.pdaBump` |
|
|
61
68
|
|
|
62
69
|
## Account data
|
|
@@ -104,6 +111,8 @@ Anchor instruction that takes or returns a struct with `String` / `Vec` / `Optio
|
|
|
104
111
|
| `system_program::transfer(CpiContext::new(…), lamports)` | `systemProgram.transfer(from, to, lamports)` |
|
|
105
112
|
| `token::transfer_checked(CpiContext::new_with_signer(…, seeds), amount, decimals)` | `tokenProgram.transferChecked(from, mint, to, authority, amount, ["seed", key, bump])` |
|
|
106
113
|
| `invoke_signed(&ix, &accounts, &[seeds])` | `invoke(program, [writable(a), readonly(b)], [data…], seeds)` |
|
|
114
|
+
| `invoke_signed(&Instruction { program_id, accounts, data }, ctx.remaining_accounts, …)` built at run time | `invokeInstruction(programId, metas, data, remainingAccounts(), seeds)` |
|
|
115
|
+
| `multisig.owners = owners` (a `Vec` argument into account data) | the same, into a `FixedVec<Pubkey, N>` |
|
|
107
116
|
| `declare_program!(other)` / CPI to another Anchor program | `typesol cpi other.json` generates typed helpers |
|
|
108
117
|
| `for x in v.iter()` | `for (const x of v)` |
|
|
109
118
|
| `v.iter().any(\|x\| …)`, `.all`, `.position`, `.find`, `.fold(init, …)`, `.contains(&x)` | `v.some((x) => …)`, `every`, `findIndex`, `find`, `reduce((acc, x) => …, init)`, `includes(x)` |
|
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.
|
|
@@ -62,6 +65,10 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
62
65
|
12. **CPIs.** A program signs only for its own PDAs (the runtime enforces it; the seeds are those written in the
|
|
63
66
|
source). `invoke` marks an account writable or signer only when the parameter is `Mut` / `Signer` or the program
|
|
64
67
|
signs for it; remaining accounts keep the flags they were passed with, or are checked for the role requested.
|
|
68
|
+
`invokeInstruction` is the exception by design: its program, account flags and data come from values (stored in
|
|
69
|
+
account data or passed in), so the compiler cannot check them; the runtime does, refusing any account the
|
|
70
|
+
transaction did not pass that way and any signer the program's seeds do not cover. Only programs that store
|
|
71
|
+
instructions to run (a multisig, a DAO) need it; review who can write those values.
|
|
65
72
|
Return data is read only from the program named, at the expected size (5106).
|
|
66
73
|
13. **Events.** `emitCpi` is accepted only from this program's event-authority PDA, signed (2002).
|
|
67
74
|
|
|
@@ -78,6 +85,10 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
78
85
|
- **`AccountInfo` / `UncheckedProgram`.** Unchecked by design, as in Anchor: check what you rely on.
|
|
79
86
|
- **`requirePda(account, seeds, bump)`.** Any valid bump passes (Anchor's `bump = expr`). Use a stored bump, never one
|
|
80
87
|
from the arguments.
|
|
88
|
+
- **`Dup<…>`.** A parameter marked `Dup` may be the same account as another writable one. Reads see every write
|
|
89
|
+
(both are the same bytes), so a value read before a write through the other slot is stale: read, then write.
|
|
90
|
+
- **Instruction introspection.** The Instructions sysvar holds what the transaction's author put there. Require things
|
|
91
|
+
of the transaction with it (that a repay instruction follows), never treat it as proof of who did what.
|
|
81
92
|
- **Oracles.** `pythPrice` checks verification, feed and age (pass the exponent you scale by); the raw `update.*`
|
|
82
93
|
fields are unchecked. `switchboardValue` needs the expected feed; set `minSamples`.
|
|
83
94
|
- **`merkleVerify`.** Leaves and inner nodes hash alike: build leaves as `keccak256(keccak256(data))` in the program.
|
|
@@ -87,7 +98,8 @@ Each holds for every instruction of every program the compiler accepts. A break
|
|
|
87
98
|
|
|
88
99
|
## Where TypeSOL is stricter than Anchor
|
|
89
100
|
|
|
90
|
-
- Any two writable parameters naming one account are refused (Anchor compares only program
|
|
101
|
+
- Any two writable parameters naming one account are refused unless one is `Dup<…>` (Anchor compares only program
|
|
102
|
+
and token accounts, and `dup` lifts that).
|
|
91
103
|
- Arithmetic overflow returns an error code (5100) in every build; Anchor aborts the program, and only when
|
|
92
104
|
overflow checks are enabled.
|
|
93
105
|
|
package/lib/typesol.d.ts
CHANGED
|
@@ -123,7 +123,8 @@ export interface FixedVec<T, N extends number> extends ListMethods<T> {
|
|
|
123
123
|
readonly length: u64;
|
|
124
124
|
get(index: bigint): T;
|
|
125
125
|
set(index: bigint, value: T): void;
|
|
126
|
-
|
|
126
|
+
// `...rest: never[]` takes nothing, but lets a Vec (whose push takes a payer) be assigned to a FixedVec: `acct.owners = owners`.
|
|
127
|
+
push(value: T, ...rest: never[]): void;
|
|
127
128
|
pop(): T;
|
|
128
129
|
clear(): void;
|
|
129
130
|
[Symbol.iterator](): Iterator<T>;
|
|
@@ -186,10 +187,43 @@ export declare function keccak256(...parts: (string | Pubkey | Hash | bigint | b
|
|
|
186
187
|
*/
|
|
187
188
|
export declare function merkleVerify(leaf: Hash, proof: Vec<Hash>, root: Hash): boolean;
|
|
188
189
|
|
|
189
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* The Instructions sysvar (Sysvar1nstructions…): every instruction of the transaction being executed. Its address is
|
|
192
|
+
* checked. What it holds is the transaction author's: use it to require something of the transaction (a repay
|
|
193
|
+
* instruction after a flash loan, a fee instruction before), never as proof of anything else.
|
|
194
|
+
*/
|
|
190
195
|
export interface InstructionsSysvar extends AccountMeta {
|
|
191
196
|
readonly __instructions_sysvar: void;
|
|
197
|
+
/** How many instructions the transaction has. */
|
|
198
|
+
readonly length: u64;
|
|
199
|
+
/** The index of the top-level instruction being executed (this one, or the one that CPI'd into this program). */
|
|
200
|
+
currentIndex(): u64;
|
|
201
|
+
/** The transaction's instruction at `index` (5104 when there is none). */
|
|
202
|
+
get(index: u64): TransactionInstruction;
|
|
192
203
|
}
|
|
204
|
+
/** One instruction of the transaction, read from the Instructions sysvar. Reads past its end fail with 5104. */
|
|
205
|
+
export interface TransactionInstruction {
|
|
206
|
+
readonly __transaction_instruction: void;
|
|
207
|
+
readonly programId: Pubkey;
|
|
208
|
+
readonly accountsLength: u64;
|
|
209
|
+
readonly dataLength: u64;
|
|
210
|
+
accountKey(i: u64): Pubkey;
|
|
211
|
+
accountIsSigner(i: u64): boolean;
|
|
212
|
+
accountIsWritable(i: u64): boolean;
|
|
213
|
+
/** Little-endian integers and keys from the instruction data. */
|
|
214
|
+
readU8(offset: u64): u8;
|
|
215
|
+
readU16(offset: u64): u16;
|
|
216
|
+
readU32(offset: u64): u32;
|
|
217
|
+
readU64(offset: u64): u64;
|
|
218
|
+
readPubkey(offset: u64): Pubkey;
|
|
219
|
+
/** `program`'s instruction `name` (snake_case or camelCase): its program id and 8-byte Anchor discriminator match. */
|
|
220
|
+
is(program: Pubkey, name: string): boolean;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* How deep in cross-program calls this program runs: 1 when the transaction calls it directly, more when another
|
|
224
|
+
* program invoked it. `require(stackHeight() === 1n, …)` refuses CPI callers.
|
|
225
|
+
*/
|
|
226
|
+
export declare function stackHeight(): u64;
|
|
193
227
|
|
|
194
228
|
/**
|
|
195
229
|
* True when this transaction contains an Ed25519 precompile instruction that verified `message`
|
|
@@ -597,6 +631,32 @@ export type TokenOf<A, MintParam extends string, AuthorityParam extends string =
|
|
|
597
631
|
export type MintOf<A, AuthorityParam extends string, FreezeAuthorityParam extends string = never, TokenProgramParam extends string = never> = A & {
|
|
598
632
|
readonly __mintOf?: [AuthorityParam, FreezeAuthorityParam, TokenProgramParam];
|
|
599
633
|
};
|
|
634
|
+
/**
|
|
635
|
+
* Anchor's `dup`: this writable account may be the same account as another writable parameter (TypeSOL refuses that
|
|
636
|
+
* otherwise, 2040). Both are read and written in place, so a write through one is seen through the other: read
|
|
637
|
+
* what you need before writing. Not for accounts that are created, closed, a close destination or `@layout borsh`.
|
|
638
|
+
*/
|
|
639
|
+
export type Dup<A> = A & { readonly __dup?: true };
|
|
640
|
+
/**
|
|
641
|
+
* The account must sign the transaction (Anchor's `signer` constraint on an account that is not a `Signer`; 2002), e.g.
|
|
642
|
+
* `Signed<Zeroed<Multisig>>` for an account the client creates with a fresh keypair in the same transaction.
|
|
643
|
+
*/
|
|
644
|
+
export type Signed<A> = A & { readonly __signed?: true };
|
|
645
|
+
/**
|
|
646
|
+
* Anchor 1.0's `Migration<From, To>`: an existing `From` account (owner, discriminator and size checked) that this
|
|
647
|
+
* instruction rewrites as `To` with `acct.migrate(payer, value)`, a top-level statement every path must reach. Read
|
|
648
|
+
* the old data through `acct.from` before migrating (the new value may use it: it is built in full before the account
|
|
649
|
+
* is rewritten), and the new data through `acct.to` after. The account is resized to `To`: the payer tops up the rent
|
|
650
|
+
* when it grows (at most 10 KiB), and shrinking keeps the lamports. `From` usually keeps the old type's discriminator
|
|
651
|
+
* under a new name: `/** @discriminator account:Counter *\/ interface CounterV1 { … }`. An account already migrated
|
|
652
|
+
* fails with 3002, as under Anchor.
|
|
653
|
+
*/
|
|
654
|
+
export interface Migrate<From, To> extends AccountMeta {
|
|
655
|
+
readonly __migrate: [From, To];
|
|
656
|
+
readonly from: Readonly<From>;
|
|
657
|
+
readonly to: To;
|
|
658
|
+
migrate(payer: Mut<Signer>, value: To): void;
|
|
659
|
+
}
|
|
600
660
|
export type Seeds<A, Names extends readonly string[], Bump extends string = never, ProgramId extends string = never> = A & {
|
|
601
661
|
readonly __seeds?: [Names, Bump, ProgramId];
|
|
602
662
|
/** The PDA's bump (Anchor `ctx.bumps.<account>`): the stored bump field when one is named, else the canonical bump found. */
|
|
@@ -1325,9 +1385,10 @@ export type Init<T> = T extends TokenAccount
|
|
|
1325
1385
|
};
|
|
1326
1386
|
|
|
1327
1387
|
/**
|
|
1328
|
-
*
|
|
1329
|
-
* owned by this program and still zeroed
|
|
1330
|
-
* the discriminator is written: from here on it is a `Mut<T>`.
|
|
1388
|
+
* An account the client already allocated with System CreateAccount (any size up to 10 MiB; over 10 KiB a program
|
|
1389
|
+
* cannot create it itself), owned by this program and still zeroed: Anchor's `zero`. Emptiness (2013), owner, size and
|
|
1390
|
+
* rent exemption are checked, then the discriminator is written: from here on it is a `Mut<T>`. A `@layout borsh`
|
|
1391
|
+
* struct starts empty (lists and strings empty, Options None) and is written as Borsh when the instruction returns.
|
|
1331
1392
|
*/
|
|
1332
1393
|
export type Zeroed<T> = T & AccountMeta;
|
|
1333
1394
|
|
|
@@ -1556,6 +1617,32 @@ export declare function invoke(
|
|
|
1556
1617
|
signerSeeds?: Seed[] | SignerSeeds,
|
|
1557
1618
|
): void;
|
|
1558
1619
|
|
|
1620
|
+
/** One account of an instruction built at run time (Rust's `AccountMeta`): any struct with these three fields. */
|
|
1621
|
+
export interface InstructionAccountMeta {
|
|
1622
|
+
readonly pubkey: Pubkey;
|
|
1623
|
+
readonly isSigner: boolean;
|
|
1624
|
+
readonly isWritable: boolean;
|
|
1625
|
+
}
|
|
1626
|
+
/** A list read by index: a Vec or FixedVec in account data, a Vec argument, or a stored local list. */
|
|
1627
|
+
export interface IndexedList<T> {
|
|
1628
|
+
readonly length: u64;
|
|
1629
|
+
get(index: bigint): T;
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* A CPI decided at run time, e.g. one stored in account data (Rust's `invoke_signed(&Instruction { program_id,
|
|
1633
|
+
* accounts, data }, infos, seeds)`). Every account in `infos` (`remainingAccounts()` or a slice of it) is handed to the
|
|
1634
|
+
* runtime, which finds each meta's account there by key: one that is missing fails the CPI, as under Rust. A meta may
|
|
1635
|
+
* be a signer or writable only as the transaction passed that account, or as a PDA of this program `signerSeeds` sign
|
|
1636
|
+
* for (`signer([multisig.key, multisig.nonce])`).
|
|
1637
|
+
*/
|
|
1638
|
+
export declare function invokeInstruction(
|
|
1639
|
+
programId: Pubkey,
|
|
1640
|
+
accounts: IndexedList<InstructionAccountMeta>,
|
|
1641
|
+
data: IndexedList<u8>,
|
|
1642
|
+
infos: RemainingAccounts | RemainingSlice,
|
|
1643
|
+
signerSeeds?: Seed[] | SignerSeeds,
|
|
1644
|
+
): void;
|
|
1645
|
+
|
|
1559
1646
|
/**
|
|
1560
1647
|
* Anchor's own error codes (anchor-lang `ErrorCode`): `require(condition, AnchorError.AccountDidNotDeserialize)` fails
|
|
1561
1648
|
* with that code, as `require!(condition, ErrorCode::AccountDidNotDeserialize)` does. Not listed in the IDL's errors.
|
package/package.json
CHANGED
package/runtime/typesol_rt.h
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
#define TS_ERR_CONSTRAINT_EXECUTABLE 2007 /* ConstraintExecutable */
|
|
28
28
|
#define TS_ERR_ASSOCIATED 2009 /* ConstraintAssociated */
|
|
29
29
|
#define TS_ERR_ADDRESS 2012 /* ConstraintAddress */
|
|
30
|
+
#define TS_ERR_CONSTRAINT_ZERO 2013 /* ConstraintZero */
|
|
30
31
|
#define TS_ERR_TOKEN_MINT 2014 /* ConstraintTokenMint */
|
|
31
32
|
#define TS_ERR_TOKEN_OWNER 2015 /* ConstraintTokenOwner */
|
|
32
33
|
#define TS_ERR_MINT_AUTHORITY 2016 /* ConstraintMintMintAuthority */
|
|
@@ -654,10 +655,15 @@ static inline uint64_t ts_now(int64_t *out) {
|
|
|
654
655
|
* to this program. It must be writable, rent-exempt, big enough and still all-zero in its discriminator;
|
|
655
656
|
* the discriminator is written here so the account can never be initialized twice.
|
|
656
657
|
*/
|
|
657
|
-
|
|
658
|
+
/* Zeroed<T> (Anchor's `zero`), in Anchor's order: a discriminator already set (2013), then owner and size as the
|
|
659
|
+
* account type checks them; the rent check and the discriminator write come after the mut / signer checks. */
|
|
660
|
+
static inline uint64_t ts_check_zeroed(const TsAccount *a, const SolPubkey *program_id, uint64_t size) {
|
|
661
|
+
if (a->data_len >= 8 && ts_ld64(a->data) != 0) TS_FAIL(TS_ERR_CONSTRAINT_ZERO, "TypeSOL: account already initialized");
|
|
658
662
|
if (!ts_key_eq(&a->owner, program_id)) TS_FAIL(TS_ERR_WRONG_OWNER, "TypeSOL: account not owned by this program");
|
|
659
663
|
if (a->data_len < 8 + size) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account data too small");
|
|
660
|
-
|
|
664
|
+
return SUCCESS;
|
|
665
|
+
}
|
|
666
|
+
static inline uint64_t ts_init_zeroed(TsAccount *a, const uint8_t disc[8]) {
|
|
661
667
|
uint64_t need;
|
|
662
668
|
TS_TRY(ts_minimum_balance(a->data_len, &need));
|
|
663
669
|
if (a->lamports < need) TS_FAIL(TS_ERR_NOT_RENT_EXEMPT, "TypeSOL: account is not rent-exempt");
|
|
@@ -1121,6 +1127,81 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
|
|
|
1121
1127
|
return ts_mem_eq(node, root, 32);
|
|
1122
1128
|
}
|
|
1123
1129
|
|
|
1130
|
+
/*
|
|
1131
|
+
* The Instructions sysvar: u16 count, u16 offsets[count], then per instruction u16 num_accounts,
|
|
1132
|
+
* num_accounts * (u8 flags: bit 0 signer, bit 1 writable; 32-byte key), 32-byte program id, u16 data_len, data; the
|
|
1133
|
+
* last two bytes of the account are the index of the instruction being executed. Everything here is the transaction
|
|
1134
|
+
* author's data: bounds-checked, never trusted for anything but what it says.
|
|
1135
|
+
*/
|
|
1136
|
+
typedef struct {
|
|
1137
|
+
const uint8_t *accounts; /* num_accounts * 33 bytes */
|
|
1138
|
+
const SolPubkey *program_id;
|
|
1139
|
+
const uint8_t *data;
|
|
1140
|
+
uint16_t num_accounts;
|
|
1141
|
+
uint16_t data_len;
|
|
1142
|
+
} TsIx;
|
|
1143
|
+
|
|
1144
|
+
/* Instruction `index` of the transaction; false when out of range or malformed. */
|
|
1145
|
+
static inline bool ts_ix_at(const TsAccount *ixs, uint64_t index, TsIx *out) {
|
|
1146
|
+
const uint8_t *d = ixs->data;
|
|
1147
|
+
uint64_t len = ixs->data_len;
|
|
1148
|
+
if (len < 2) return false;
|
|
1149
|
+
uint16_t count;
|
|
1150
|
+
ts_memcpy(&count, d, 2);
|
|
1151
|
+
if (index >= count || 2 + (uint64_t)count * 2 > len) return false;
|
|
1152
|
+
uint16_t at;
|
|
1153
|
+
ts_memcpy(&at, d + 2 + index * 2, 2);
|
|
1154
|
+
uint64_t p = at;
|
|
1155
|
+
if (p + 2 > len) return false;
|
|
1156
|
+
ts_memcpy(&out->num_accounts, d + p, 2);
|
|
1157
|
+
p += 2;
|
|
1158
|
+
out->accounts = d + p;
|
|
1159
|
+
p += (uint64_t)out->num_accounts * 33;
|
|
1160
|
+
if (p + 34 > len) return false;
|
|
1161
|
+
out->program_id = (const SolPubkey *)(d + p);
|
|
1162
|
+
p += 32;
|
|
1163
|
+
ts_memcpy(&out->data_len, d + p, 2);
|
|
1164
|
+
p += 2;
|
|
1165
|
+
if (p + out->data_len > len) return false;
|
|
1166
|
+
out->data = d + p;
|
|
1167
|
+
return true;
|
|
1168
|
+
}
|
|
1169
|
+
static inline uint64_t ts_ix_load(const TsAccount *ixs, uint64_t index, TsIx *out) {
|
|
1170
|
+
if (!ts_ix_at(ixs, index, out)) TS_FAIL(TS_ERR_INDEX, "TypeSOL: no such instruction in the transaction");
|
|
1171
|
+
return SUCCESS;
|
|
1172
|
+
}
|
|
1173
|
+
static inline uint64_t ts_ix_count(const TsAccount *ixs, uint64_t *out) {
|
|
1174
|
+
if (ixs->data_len < 2) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
|
|
1175
|
+
uint16_t n;
|
|
1176
|
+
ts_memcpy(&n, ixs->data, 2);
|
|
1177
|
+
*out = n;
|
|
1178
|
+
return SUCCESS;
|
|
1179
|
+
}
|
|
1180
|
+
static inline uint64_t ts_ix_current(const TsAccount *ixs, uint64_t *out) {
|
|
1181
|
+
if (ixs->data_len < 4) TS_FAIL(TS_ERR_INDEX, "TypeSOL: empty instructions sysvar");
|
|
1182
|
+
uint16_t i;
|
|
1183
|
+
ts_memcpy(&i, ixs->data + ixs->data_len - 2, 2);
|
|
1184
|
+
*out = i;
|
|
1185
|
+
return SUCCESS;
|
|
1186
|
+
}
|
|
1187
|
+
static inline uint64_t ts_ix_account(const TsIx *ix, uint64_t i, const uint8_t **out) {
|
|
1188
|
+
if (i >= ix->num_accounts) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the instruction has no account at that index");
|
|
1189
|
+
*out = ix->accounts + i * 33;
|
|
1190
|
+
return SUCCESS;
|
|
1191
|
+
}
|
|
1192
|
+
/* `n` bytes of the instruction's data at `off` (little-endian integers, keys). */
|
|
1193
|
+
static inline uint64_t ts_ix_read(const TsIx *ix, uint64_t off, uint64_t n, void *out) {
|
|
1194
|
+
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");
|
|
1195
|
+
ts_memcpy(out, ix->data + off, n);
|
|
1196
|
+
return SUCCESS;
|
|
1197
|
+
}
|
|
1198
|
+
/* Is it `program`'s instruction with this 8-byte discriminator (Anchor's sha256("global:<name>")[..8])? */
|
|
1199
|
+
static inline bool ts_ix_is(const TsIx *ix, const SolPubkey *program, const uint8_t disc[8]) {
|
|
1200
|
+
return ts_key_eq(ix->program_id, program) && ix->data_len >= 8 && ts_mem_eq(ix->data, disc, 8);
|
|
1201
|
+
}
|
|
1202
|
+
/* How deep in CPIs the program runs: 1 when the transaction calls it directly. */
|
|
1203
|
+
extern uint64_t sol_get_stack_height(void);
|
|
1204
|
+
|
|
1124
1205
|
/*
|
|
1125
1206
|
* Did an Ed25519 precompile instruction in this transaction verify `message` signed by `signer`?
|
|
1126
1207
|
* Reads the Instructions sysvar (data: u16 count, u16 offsets[count], then per instruction
|
|
@@ -1129,29 +1210,15 @@ static inline bool ts_merkle_verify(const SolPubkey *leaf, const uint8_t *proof,
|
|
|
1129
1210
|
* message, with offsets pointing into the precompile's own data (instruction index 0xFFFF).
|
|
1130
1211
|
*/
|
|
1131
1212
|
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;
|
|
1213
|
+
if (ixs->data_len < 2) return false;
|
|
1135
1214
|
uint16_t count;
|
|
1136
|
-
ts_memcpy(&count,
|
|
1137
|
-
if (2 + (uint64_t)count * 2 > len) return false;
|
|
1215
|
+
ts_memcpy(&count, ixs->data, 2);
|
|
1138
1216
|
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;
|
|
1217
|
+
TsIx t;
|
|
1218
|
+
if (!ts_ix_at(ixs, i, &t)) return false;
|
|
1219
|
+
if (!ts_key_eq(t.program_id, &TS_ED25519_PROGRAM_ID) || t.data_len < 2) continue;
|
|
1220
|
+
uint16_t dlen = t.data_len;
|
|
1221
|
+
const uint8_t *ix = t.data;
|
|
1155
1222
|
uint8_t nsig = ix[0];
|
|
1156
1223
|
for (uint8_t k = 0; k < nsig; k++) {
|
|
1157
1224
|
uint64_t o = 2 + (uint64_t)k * 14;
|