@mysten/docs 0.1.33 → 0.1.34
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/dist/llms-index.md +17 -4
- package/dist/sponsor/basic-usage.md +170 -0
- package/dist/sponsor/best-practices.md +163 -0
- package/dist/sponsor/index.md +114 -0
- package/dist/sponsor/llms-index.md +7 -0
- package/dist/sponsor/validators.md +198 -0
- package/dist/sui/cryptography/index.md +225 -0
- package/dist/sui/cryptography/keypairs.md +36 -143
- package/dist/sui/cryptography/signers/aws-kms.md +110 -0
- package/dist/sui/cryptography/signers/gcp-kms.md +91 -0
- package/dist/sui/cryptography/signers/index.md +78 -0
- package/dist/sui/cryptography/signers/ledger.md +76 -0
- package/dist/sui/cryptography/{multisig.md → signers/multisig.md} +2 -2
- package/dist/sui/cryptography/{passkey.md → signers/passkey.md} +1 -1
- package/dist/sui/cryptography/{webcrypto-signer.md → signers/webcrypto.md} +6 -6
- package/dist/sui/index.md +4 -5
- package/dist/sui/llms-index.md +9 -4
- package/dist/sui/migrations/sui-2.0/index.md +14 -1
- package/dist/sui/zklogin.md +2 -2
- package/dist/walrus/index.md +6 -0
- package/package.json +21 -21
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
> Compose built-in and custom validation analyzers for sponsorship policy
|
|
4
|
+
|
|
5
|
+
A validator is an analyzer used for policy checks. Create one with `createAnalyzer(...)`, return the
|
|
6
|
+
issues it found, and pass it to `createSponsor({ validate })`. It declares the analyzers it reads
|
|
7
|
+
through `dependencies` and reports one of three outcomes:
|
|
8
|
+
|
|
9
|
+
- **pass**: `{ result: null }` (or `{ result: [] }`);
|
|
10
|
+
- **reject**: `{ result: [{ code, message }] }`: the transaction is well-formed but violates policy
|
|
11
|
+
(`POLICY_REJECTED`);
|
|
12
|
+
- **couldn't analyze**: `{ issues: [{ message }] }` or `throw`: the analyzer itself couldn't decide
|
|
13
|
+
(a failed lookup, an unreachable service), producing `ANALYSIS_FAILED`. This is the analyzer
|
|
14
|
+
framework's own channel, so it propagates, and dependents don't run.
|
|
15
|
+
|
|
16
|
+
Reporting findings as the `result` (rather than through `issues`) is what keeps "violates policy"
|
|
17
|
+
distinct from "couldn't be checked". There's no built-in "the sponsor must be paid" rule, because
|
|
18
|
+
value-flow policy is app-specific, so write it over the built-in `balanceFlows` analyzer (signed
|
|
19
|
+
per-address deltas: **negative = value left the owner, positive = arrived**):
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
|
|
23
|
+
const USDC = '0x...::usdc::USDC';
|
|
24
|
+
|
|
25
|
+
const requireSponsorPayment = createAnalyzer({
|
|
26
|
+
dependencies: { balanceFlows: analyzers.balanceFlows },
|
|
27
|
+
analyze:
|
|
28
|
+
() =>
|
|
29
|
+
({ balanceFlows }) => {
|
|
30
|
+
const received =
|
|
31
|
+
balanceFlows.sponsor
|
|
32
|
+
?.filter((flow) => flow.coinType === USDC)
|
|
33
|
+
.reduce((sum, flow) => sum + flow.amount, 0n) ?? 0n;
|
|
34
|
+
|
|
35
|
+
return received < 10_000n
|
|
36
|
+
? { result: [{ code: 'UNDERPAID', message: `Sponsor received ${received}, needs 10000.` }] }
|
|
37
|
+
: { result: null };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The most useful shared analyzer is **`data`**, the parsed transaction (sender, gas data, commands,
|
|
43
|
+
expiration), which most validators read. Alongside it: `balanceFlows` (signed value deltas),
|
|
44
|
+
`transactionResponse` (the dry-run, including effects), `commands`, `moveFunctions`, `objects`,
|
|
45
|
+
`coins`, `inputs`, and `bytes`, plus the sponsor's `currentEpoch`, and any others the analyzer
|
|
46
|
+
package adds (see [`@mysten/wallet-sdk`](https://www.npmjs.com/package/@mysten/wallet-sdk) for the
|
|
47
|
+
full set). All are re-exported as `analyzers` (with `currentEpoch` and `createAnalyzer` alongside).
|
|
48
|
+
A failed analyzer never reaches your `analyze`: it short-circuits to an `ANALYSIS_FAILED` rejection.
|
|
49
|
+
|
|
50
|
+
## Loading data, and sharing it across validators
|
|
51
|
+
|
|
52
|
+
An analyzer receives the same `options` the sponsor passes to `analyze` (including **`client`**), so
|
|
53
|
+
it can load onchain data. And because the framework runs each analyzer once and shares its result,
|
|
54
|
+
several validators read it for the cost of one fetch:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
|
|
58
|
+
// Loads the *sponsor's* SUI balance via options.client (the gas owner is the sponsor)…
|
|
59
|
+
const sponsorBalance = createAnalyzer({
|
|
60
|
+
dependencies: { data: analyzers.data },
|
|
61
|
+
analyze:
|
|
62
|
+
(options) =>
|
|
63
|
+
async ({ data }) => {
|
|
64
|
+
const { balance } = await options.client.core.getBalance({ owner: data.gasData.owner! });
|
|
65
|
+
return { result: BigInt(balance.balance) };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// …read by two validators; the `getBalance` call still runs only once.
|
|
70
|
+
const sponsorCanCoverGas = createAnalyzer({
|
|
71
|
+
dependencies: { sponsorBalance, data: analyzers.data },
|
|
72
|
+
analyze:
|
|
73
|
+
() =>
|
|
74
|
+
({ sponsorBalance, data }) =>
|
|
75
|
+
sponsorBalance >= BigInt(data.gasData.budget ?? 0)
|
|
76
|
+
? { result: null }
|
|
77
|
+
: {
|
|
78
|
+
result: [{ code: 'SPONSOR_UNDERFUNDED', message: 'Sponsor balance cannot cover gas.' }],
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const sponsorKeepsReserve = createAnalyzer({
|
|
82
|
+
dependencies: { sponsorBalance },
|
|
83
|
+
analyze:
|
|
84
|
+
() =>
|
|
85
|
+
({ sponsorBalance }) =>
|
|
86
|
+
sponsorBalance >= 1_000_000_000n
|
|
87
|
+
? { result: null }
|
|
88
|
+
: { result: [{ code: 'RESERVE_LOW', message: 'Sponsor reserve below 1 SUI.' }] },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
createSponsor({ signer, client, validate: [sponsorCanCoverGas, sponsorKeepsReserve] });
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Request-scoped options
|
|
95
|
+
|
|
96
|
+
A validator reads request inputs (an auth token, a tenant id) straight off `options`.
|
|
97
|
+
`createSponsor` **infers** them and requires them, typed, under `validationOptions` on sponsor calls
|
|
98
|
+
(required only when the option itself is required), without an untyped metadata bag:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const authChecked = createAnalyzer({
|
|
102
|
+
analyze: (options: { authToken: string }) => () =>
|
|
103
|
+
isValidToken(options.authToken)
|
|
104
|
+
? { result: null }
|
|
105
|
+
: { result: [{ code: 'BAD_AUTH', message: 'Invalid auth token.' }] },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const sponsor = createSponsor({ signer, client, validate: [authChecked] });
|
|
109
|
+
|
|
110
|
+
// `validationOptions.authToken` is now a required, typed argument:
|
|
111
|
+
await sponsor.signAndExecuteTransaction({
|
|
112
|
+
transaction,
|
|
113
|
+
userSignature,
|
|
114
|
+
validationOptions: { authToken },
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## How it runs
|
|
119
|
+
|
|
120
|
+
`createSponsor` makes every validator a dependency of **`sponsor.analyzer`**, and validation is just
|
|
121
|
+
`analyze({ check: sponsor.analyzer }, { transaction, client })`. The analyzer framework then
|
|
122
|
+
handles:
|
|
123
|
+
|
|
124
|
+
- **Lazy**: only analyzers some validator depends on run, so cost tracks your policy. `defaults()`
|
|
125
|
+
includes `simulationSucceeds`, so the default config _does_ dry-run; drop it (or use only
|
|
126
|
+
validators that read `data`) and the sponsor **never simulates**. There's no "offline phase" to
|
|
127
|
+
declare, because it falls out of the dependency graph.
|
|
128
|
+
- **Deduped**: `data` and `balanceFlows` and so on resolve once even when many validators (and a
|
|
129
|
+
host graph) depend on them.
|
|
130
|
+
- **Propagated**: a failed analyzer becomes an `ANALYSIS_FAILED` rejection rather than reaching a
|
|
131
|
+
validator with a broken value.
|
|
132
|
+
|
|
133
|
+
When validation fails, the sponsor never signs and the method **returns**
|
|
134
|
+
`{ $kind: 'Rejected', issues, reason }`, where `issues` lists every reason, and `reason` is
|
|
135
|
+
`'POLICY_REJECTED'` or `'ANALYSIS_FAILED'`. To turn a rejection into a thrown error, the exported
|
|
136
|
+
`SponsorValidationError` class takes `(issues, reason)`.
|
|
137
|
+
|
|
138
|
+
**`sponsor.analyzer`** is also the composable handle: drop it into any other `analyze()` graph and
|
|
139
|
+
it contributes `SponsorRejection | null`, deduping its analyzers with that graph.
|
|
140
|
+
|
|
141
|
+
## Built-in validation analyzers
|
|
142
|
+
|
|
143
|
+
| Validator | Reads | Rejects when… |
|
|
144
|
+
| ------------------------------ | ---------------------- | --------------------------------------------------------- |
|
|
145
|
+
| `validSender()` | `data` | the sender is unset, or is the gas owner (sponsor) |
|
|
146
|
+
| `onlyAddressBalanceGas()` | `data` | the gas payment isn't empty (`[]`)† |
|
|
147
|
+
| `gasCoinNotUsed()` | `data` | a command uses the gas coin (`tx.gas`) |
|
|
148
|
+
| `onlySenderWithdrawals()` | `data` | a `FundsWithdrawal` input isn't the sender's |
|
|
149
|
+
| `userSignatureMatchesSender()` | `bytes`, `data` | a supplied user signature isn't a valid sender signature‡ |
|
|
150
|
+
| `gasBudget({ min?, max? })` | `data` | the gas budget is unset or outside the range |
|
|
151
|
+
| `allowedPackages([...])` | `data` | a MoveCall targets a package outside the allowlist |
|
|
152
|
+
| `allowedFunctions([...])` | `data` | a MoveCall targets a function outside the allowlist |
|
|
153
|
+
| `simulationSucceeds()` | `transactionResponse` | the dry-run succeeds but the transaction would abort\* |
|
|
154
|
+
| `boundedExpiration()` | `data`, `currentEpoch` | the expiration is missing or beyond the next epoch |
|
|
155
|
+
|
|
156
|
+
\* The dry-run itself _succeeding_ but reporting an aborting transaction is a **policy** rejection
|
|
157
|
+
(`TRANSACTION_WOULD_FAIL`): the bytes are executable and would still cost the sponsor gas (landing a
|
|
158
|
+
failed transaction with a digest) if submitted. The dry-run _failing to run at all_ (an unreachable
|
|
159
|
+
node, unresolvable objects) is instead surfaced as `ANALYSIS_FAILED`, with the underlying error
|
|
160
|
+
detail.
|
|
161
|
+
|
|
162
|
+
† Address-balance gas (an empty payment) is how the sponsor pays from its own balance rather than
|
|
163
|
+
from nominated gas coins; the sponsor-builds flow always sets this, so this validator mainly guards
|
|
164
|
+
user-supplied bytes.
|
|
165
|
+
|
|
166
|
+
‡ Verifies (through `@mysten/sui`'s `isValidTransactionSignature`) that **every** supplied user
|
|
167
|
+
signature is cryptographically valid over the bytes **and** resolves to the sender, caught before
|
|
168
|
+
the sponsor co-signs, rather than only at execution (all supplied signatures are attached to
|
|
169
|
+
execution, so one that isn't the sender's would be rejected onchain after the sponsor signed). A
|
|
170
|
+
malformed, invalid, or wrong-signer signature is rejected as `USER_SIGNATURE_INVALID`; the sender
|
|
171
|
+
match is key-type aware (a zkLogin key matches either its legacy or current address). An
|
|
172
|
+
_environmental_ failure during verification (for example, a zkLogin JWK or epoch lookup throwing)
|
|
173
|
+
isn't a validation result; it surfaces as `ANALYSIS_FAILED`, so a network blip is never reported as
|
|
174
|
+
a bad signature. Passes when no user signature was supplied (the sponsor-builds flow). The signature
|
|
175
|
+
is read from the request, not from `validationOptions`.
|
|
176
|
+
|
|
177
|
+
`defaults()` bundles `validSender()` + `onlyAddressBalanceGas()` + `gasCoinNotUsed()` +
|
|
178
|
+
`onlySenderWithdrawals()` + `simulationSucceeds()` + `boundedExpiration()`.
|
|
179
|
+
|
|
180
|
+
## Timing-attack mitigation
|
|
181
|
+
|
|
182
|
+
Optionally insert random delays to reduce timing manipulation of onchain state between signing and
|
|
183
|
+
execution:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
createSponsor({
|
|
187
|
+
signer,
|
|
188
|
+
client,
|
|
189
|
+
delay: {
|
|
190
|
+
beforeSimulate: { min: 50, max: 200 }, // ms (or a fixed number) before the analysis is resolved
|
|
191
|
+
beforeExecute: { min: 50, max: 200 }, // before executing
|
|
192
|
+
// random: () => 0.5, // override the RNG (e.g. in tests)
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
This is mitigation, not prevention. `beforeSimulate` runs once before the analysis resolves (where
|
|
198
|
+
simulation, if any, happens). Default is off.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Cryptography
|
|
2
|
+
|
|
3
|
+
> Sign and verify Sui transactions and messages with keypairs and external signers
|
|
4
|
+
|
|
5
|
+
A signature authorizes every transaction and personal message on Sui. The Sui TypeScript SDK exposes
|
|
6
|
+
a single abstraction for producing those signatures, the `Signer`, along with several
|
|
7
|
+
implementations of it, from in-process keypairs to hardware wallets and cloud key-management
|
|
8
|
+
services (KMS). This page walks through the concepts shared by every signer; the subpages cover each
|
|
9
|
+
implementation in detail.
|
|
10
|
+
|
|
11
|
+
## Signing schemes
|
|
12
|
+
|
|
13
|
+
A signer always produces a signature under a signature scheme, which determines the curve and
|
|
14
|
+
signature format. Sui supports three schemes, each with a corresponding `Keypair` class:
|
|
15
|
+
|
|
16
|
+
| Scheme | Class | Import |
|
|
17
|
+
| --------------- | ------------------ | -------------------------------- |
|
|
18
|
+
| Ed25519 | `Ed25519Keypair` | `@mysten/sui/keypairs/ed25519` |
|
|
19
|
+
| ECDSA Secp256k1 | `Secp256k1Keypair` | `@mysten/sui/keypairs/secp256k1` |
|
|
20
|
+
| ECDSA Secp256r1 | `Secp256r1Keypair` | `@mysten/sui/keypairs/secp256r1` |
|
|
21
|
+
|
|
22
|
+
For background on the schemes themselves, see the
|
|
23
|
+
[Signatures](https://docs.sui.io/concepts/cryptography/transaction-auth/signatures) concept topic. A
|
|
24
|
+
signer's scheme determines the format of its public key and the Sui address it derives.
|
|
25
|
+
|
|
26
|
+
## The Signer interface
|
|
27
|
+
|
|
28
|
+
`Signer` is an abstract class exported from `@mysten/sui/cryptography`. Every signer, whether it
|
|
29
|
+
holds the private key in memory, on a Ledger device, or in AWS KMS, implements the same interface,
|
|
30
|
+
so the rest of your code doesn't need to know where the key lives.
|
|
31
|
+
|
|
32
|
+
Implementations provide three primitives:
|
|
33
|
+
|
|
34
|
+
| Method | Description |
|
|
35
|
+
| ---------------- | ------------------------------------------------------------------ |
|
|
36
|
+
| `getPublicKey()` | Returns the `PublicKey` for deriving the address and verifying |
|
|
37
|
+
| `getKeyScheme()` | Returns the signature scheme (`ED25519`, `Secp256k1`, `Secp256r1`) |
|
|
38
|
+
| `sign(bytes)` | Signs raw bytes and returns the signature |
|
|
39
|
+
|
|
40
|
+
On top of those, `Signer` provides the methods you typically call:
|
|
41
|
+
|
|
42
|
+
| Method | Description |
|
|
43
|
+
| ----------------------------- | ------------------------------------------------ |
|
|
44
|
+
| `toSuiAddress()` | The Sui address derived from the public key |
|
|
45
|
+
| `signTransaction(bytes)` | Signs transaction bytes with the correct intent |
|
|
46
|
+
| `signPersonalMessage(bytes)` | Signs a personal message |
|
|
47
|
+
| `signAndExecuteTransaction()` | Signs and submits a transaction through a client |
|
|
48
|
+
|
|
49
|
+
There are two broad families of signer:
|
|
50
|
+
|
|
51
|
+
- **Key pairs** hold the private key in process. `Keypair` extends `Signer` and adds secret-key
|
|
52
|
+
import and export (`getSecretKey`, `fromSecretKey`). This is the easiest option for scripts,
|
|
53
|
+
backends, and tests. See [Key pairs](/sui/cryptography/keypairs).
|
|
54
|
+
- **External signers** keep the private key outside your application, in a KMS, on a hardware
|
|
55
|
+
wallet, or as a non-extractable browser key. They extend `Signer` directly. See
|
|
56
|
+
[Signers](/sui/cryptography/signers).
|
|
57
|
+
|
|
58
|
+
Because both families share the `Signer` interface, you can pass any signer wherever the SDK accepts
|
|
59
|
+
one, for example as the `signer` argument to a client's `signAndExecuteTransaction`.
|
|
60
|
+
|
|
61
|
+
## Public keys and addresses
|
|
62
|
+
|
|
63
|
+
Every signer exposes a `PublicKey` through `getPublicKey()`. You use the public key to derive the
|
|
64
|
+
signer's Sui address and to verify signatures:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
|
|
68
|
+
const keypair = new Ed25519Keypair();
|
|
69
|
+
|
|
70
|
+
// Derive the address directly from the signer...
|
|
71
|
+
const address = keypair.toSuiAddress();
|
|
72
|
+
|
|
73
|
+
// ...or from its public key
|
|
74
|
+
const publicKey = keypair.getPublicKey();
|
|
75
|
+
const sameAddress = publicKey.toSuiAddress();
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Signing messages and transactions
|
|
79
|
+
|
|
80
|
+
The signing API is identical for every signer; only construction differs. Use `signPersonalMessage`
|
|
81
|
+
to sign arbitrary bytes and `signTransaction` to sign built transaction bytes:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const message = new TextEncoder().encode('hello world');
|
|
85
|
+
const { signature } = await keypair.signPersonalMessage(message);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Both `signPersonalMessage` and `signTransaction` resolve to `{ bytes, signature }`, where
|
|
89
|
+
`signature` is the serialized signature string used for execution and verification, and `bytes` is
|
|
90
|
+
the base64 payload that the signer signed.
|
|
91
|
+
|
|
92
|
+
To sign and submit a transaction in one step, pass the signer to a client:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
|
|
96
|
+
const client = new SuiGrpcClient({
|
|
97
|
+
network: 'testnet',
|
|
98
|
+
baseUrl: 'https://fullnode.testnet.sui.io:443',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const result = await client.signAndExecuteTransaction({
|
|
102
|
+
transaction,
|
|
103
|
+
signer: keypair,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// signAndExecuteTransaction resolves to a discriminated union, so check for failure first
|
|
107
|
+
if (result.FailedTransaction) {
|
|
108
|
+
throw new Error('Transaction failed to execute');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log('Digest:', result.Transaction.digest);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Verifying signatures
|
|
115
|
+
|
|
116
|
+
Verification confirms a signature is valid for a given message and that the expected key produced
|
|
117
|
+
it. The `@mysten/sui/verify` module verifies a signature against a message and recovers the
|
|
118
|
+
`PublicKey` that produced it, without needing the original signer.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
|
|
122
|
+
const keypair = new Ed25519Keypair();
|
|
123
|
+
const message = new TextEncoder().encode('hello world');
|
|
124
|
+
const { signature } = await keypair.signPersonalMessage(message);
|
|
125
|
+
|
|
126
|
+
const publicKey = await verifyPersonalMessageSignature(message, signature);
|
|
127
|
+
|
|
128
|
+
if (!publicKey.verifyAddress(keypair.toSuiAddress())) {
|
|
129
|
+
throw new Error('Signature was valid, but was signed by a different key pair');
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Verifying that a signature is valid for a specific address
|
|
134
|
+
|
|
135
|
+
`verifyPersonalMessageSignature` and `verifyTransactionSignature` accept an optional `address` and
|
|
136
|
+
throw if the signature is not valid for it:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
|
|
140
|
+
const keypair = new Ed25519Keypair();
|
|
141
|
+
const message = new TextEncoder().encode('hello world');
|
|
142
|
+
const { signature } = await keypair.signPersonalMessage(message);
|
|
143
|
+
|
|
144
|
+
await verifyPersonalMessageSignature(message, signature, {
|
|
145
|
+
address: keypair.toSuiAddress(),
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Verifying transaction signatures
|
|
150
|
+
|
|
151
|
+
Verifying transaction signatures works the same way, using `verifyTransactionSignature`:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
|
|
155
|
+
const client = new SuiGrpcClient({
|
|
156
|
+
network: 'testnet',
|
|
157
|
+
baseUrl: 'https://fullnode.testnet.sui.io:443',
|
|
158
|
+
});
|
|
159
|
+
const tx = new Transaction();
|
|
160
|
+
// ... add some transactions...
|
|
161
|
+
const bytes = await tx.build({ client });
|
|
162
|
+
|
|
163
|
+
const keypair = new Ed25519Keypair();
|
|
164
|
+
const { signature } = await keypair.signTransaction(bytes);
|
|
165
|
+
|
|
166
|
+
await verifyTransactionSignature(bytes, signature, {
|
|
167
|
+
// optionally verify that the signature is valid for a specific address
|
|
168
|
+
address: keypair.toSuiAddress(),
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Verifying zkLogin signatures
|
|
173
|
+
|
|
174
|
+
You can't verify zkLogin signatures purely on the client. When verifying a zkLogin signature, the
|
|
175
|
+
SDK uses the GraphQL API. This works for Mainnet signatures without any additional configuration.
|
|
176
|
+
For Testnet signatures, provide a Testnet GraphQL client:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
|
|
180
|
+
const publicKey = await verifyPersonalMessageSignature(message, zkSignature, {
|
|
181
|
+
client: new SuiGraphQLClient({
|
|
182
|
+
url: 'https://graphql.testnet.sui.io/graphql',
|
|
183
|
+
}),
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
For some zkLogin accounts, there are two valid addresses for a given set of inputs, so comparing the
|
|
188
|
+
address from `publicKey.toSuiAddress()` directly against an expected address can fail. Instead, pass
|
|
189
|
+
the expected address during verification, or use `publicKey.verifyAddress(address)`:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
|
|
193
|
+
const publicKey = await verifyPersonalMessageSignature(message, zkSignature, {
|
|
194
|
+
client: new SuiGraphQLClient({
|
|
195
|
+
url: 'https://graphql.testnet.sui.io/graphql',
|
|
196
|
+
}),
|
|
197
|
+
// Throws if the signature is not valid for the provided address
|
|
198
|
+
address: '0x...expectedAddress',
|
|
199
|
+
});
|
|
200
|
+
// or
|
|
201
|
+
|
|
202
|
+
if (!publicKey.verifyAddress('0x...expectedAddress')) {
|
|
203
|
+
throw new Error('Signature was valid, but was signed by a different key pair');
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Both methods check the signature against the standard and
|
|
208
|
+
[legacy versions of the zkLogin address](https://sdk.mystenlabs.com/sui/zklogin#legacy-addresses).
|
|
209
|
+
|
|
210
|
+
## Choosing an implementation
|
|
211
|
+
|
|
212
|
+
- **[Key pairs](/sui/cryptography/keypairs)**: `Ed25519Keypair`, `Secp256k1Keypair`, and
|
|
213
|
+
`Secp256r1Keypair` hold the private key in process. The easiest option when you manage the secret
|
|
214
|
+
key yourself. Covers deriving keypairs from mnemonics and secret keys.
|
|
215
|
+
- **[Signers](/sui/cryptography/signers)**: other implementations of the `Signer` interface,
|
|
216
|
+
including signer packages that keep the private key outside your application — cloud KMS
|
|
217
|
+
([AWS](/sui/cryptography/signers/aws-kms), [GCP](/sui/cryptography/signers/gcp-kms)), a
|
|
218
|
+
[Ledger](/sui/cryptography/signers/ledger) hardware wallet, and non-extractable browser keys with
|
|
219
|
+
the [Web Crypto](/sui/cryptography/signers/webcrypto) signer — as well as
|
|
220
|
+
[passkeys](/sui/cryptography/signers/passkey) and
|
|
221
|
+
[multi-signature](/sui/cryptography/signers/multisig).
|
|
222
|
+
|
|
223
|
+
If you just need to sign in a script or server and are comfortable managing the secret key yourself,
|
|
224
|
+
start with [Key pairs](/sui/cryptography/keypairs). If the key must never live in your application's
|
|
225
|
+
memory, use one of the external [signers](/sui/cryptography/signers).
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Key pairs
|
|
2
2
|
|
|
3
|
-
> Create
|
|
3
|
+
> Create Ed25519, Secp256k1, and Secp256r1 keypairs and derive them from mnemonics and secret keys
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
A `Keypair` holds a Sui private key in process and signs with it. It is the easiest
|
|
6
|
+
[`Signer`](/sui/cryptography) to use. It is ideal for scripts, backends, and tests where you manage
|
|
7
|
+
the secret key yourself. For the signing and verification API shared by all signers, see
|
|
8
|
+
[Cryptography](/sui/cryptography); this page focuses on creating keypairs and deriving them from
|
|
9
|
+
mnemonics and secret keys.
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
Each signing scheme has a corresponding `Keypair` class:
|
|
9
12
|
|
|
10
13
|
| Sign scheme | Class name | Import folder |
|
|
11
14
|
| --------------- | ------------------ | -------------------------------- |
|
|
@@ -13,9 +16,6 @@ The Sui TypeScript SDK supports three signing schemes:
|
|
|
13
16
|
| ECDSA Secp256k1 | `Secp256k1Keypair` | `@mysten/sui/keypairs/secp256k1` |
|
|
14
17
|
| ECDSA Secp256r1 | `Secp256r1Keypair` | `@mysten/sui/keypairs/secp256r1` |
|
|
15
18
|
|
|
16
|
-
For information on these schemes, see the
|
|
17
|
-
[Signatures](https://docs.sui.io/concepts/cryptography/transaction-auth/signatures) topic.
|
|
18
|
-
|
|
19
19
|
To use, import the key pair class your project uses from the `@mysten/sui/keypairs` folder. For
|
|
20
20
|
example, to use the Ed25519 scheme, import the `Ed25519Keypair` class from
|
|
21
21
|
`@mysten/sui/keypairs/ed25519`.
|
|
@@ -24,155 +24,55 @@ example, to use the Ed25519 scheme, import the `Ed25519Keypair` class from
|
|
|
24
24
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
+
## Creating a key pair
|
|
28
|
+
|
|
27
29
|
To create a random key pair (which identifies a Sui address), instantiate a new `Keypair` class. To
|
|
28
30
|
reference a key pair from an existing secret key, pass the secret to the `fromSecretKey` function.
|
|
29
31
|
|
|
30
32
|
```typescript
|
|
31
33
|
// random Keypair
|
|
32
34
|
const keypair = new Ed25519Keypair();
|
|
33
|
-
// Keypair from an existing secret key (Uint8Array)
|
|
34
|
-
const keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
With your key pair created, you can reference it when performing actions on the network. For
|
|
38
|
-
example, you can use it to sign transactions, like the following code that creates and signs a
|
|
39
|
-
personal message using the public key from the key pair created in the previous code:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
const publicKey = keypair.getPublicKey();
|
|
43
|
-
const message = new TextEncoder().encode('hello world');
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
const
|
|
36
|
+
// or, a Keypair from an existing secret key (Uint8Array)
|
|
37
|
+
const fromSecret = Ed25519Keypair.fromSecretKey(secretKey);
|
|
47
38
|
```
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Each `Keypair` has an associated `PublicKey` class. You use the public key to verify signatures or
|
|
52
|
-
to retrieve its associated Sui address. You can access a `Keypair` from its `PublicKey` or construct
|
|
53
|
-
it from the bytes (as a `Uint8Array`) of the `PublicKey`, as in the following code:
|
|
40
|
+
With your key pair created, you can derive its address and sign with it. The following signs a
|
|
41
|
+
personal message and verifies it with the public key:
|
|
54
42
|
|
|
55
43
|
```typescript
|
|
56
|
-
|
|
57
|
-
const keypair = new Ed25519Keypair();
|
|
58
|
-
|
|
59
|
-
// method 1
|
|
60
|
-
const bytes = keypair.getPublicKey().toRawBytes();
|
|
61
|
-
const publicKey = new Ed25519PublicKey(bytes);
|
|
62
|
-
const address = publicKey.toSuiAddress();
|
|
63
|
-
|
|
64
|
-
// method 2
|
|
65
|
-
const address = keypair.getPublicKey().toSuiAddress();
|
|
66
|
-
// or use `toSuiAddress()` directly
|
|
44
|
+
const publicKey = keypair.getPublicKey();
|
|
67
45
|
const address = keypair.toSuiAddress();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Verifying signatures without a key pair
|
|
71
|
-
|
|
72
|
-
When you have an existing public key, you can use it to verify a signature. Verification ensures the
|
|
73
|
-
signature is valid for the provided message and is signed with the appropriate secret key.
|
|
74
46
|
|
|
75
|
-
The following code creates a key pair in the Ed25519 scheme, creates and signs a message with it,
|
|
76
|
-
then verifies the message to retrieve the public key. The code then uses `toSuiAddress()` to check
|
|
77
|
-
if the address associated with the public key matches the address that the key pair defines.
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
|
|
81
|
-
const keypair = new Ed25519Keypair();
|
|
82
47
|
const message = new TextEncoder().encode('hello world');
|
|
83
48
|
const { signature } = await keypair.signPersonalMessage(message);
|
|
84
|
-
|
|
85
|
-
const publicKey = await verifyPersonalMessageSignature(message, signature);
|
|
86
|
-
|
|
87
|
-
if (!publicKey.verifyAddress(keypair.toSuiAddress())) {
|
|
88
|
-
throw new Error('Signature was valid, but was signed by a different key pair');
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Verifying that a signature is valid for a specific address
|
|
93
|
-
|
|
94
|
-
`verifyPersonalMessageSignature` and `verifyTransactionSignature` accept an optional `address`, and
|
|
95
|
-
will throw an error if the signature is not valid for the provided address.
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
|
|
99
|
-
const keypair = new Ed25519Keypair();
|
|
100
|
-
const message = new TextEncoder().encode('hello world');
|
|
101
|
-
const { signature } = await keypair.signPersonalMessage(message);
|
|
102
|
-
|
|
103
|
-
await verifyPersonalMessageSignature(message, signature, {
|
|
104
|
-
address: keypair.toSuiAddress(),
|
|
105
|
-
});
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Verifying transaction signatures
|
|
109
|
-
|
|
110
|
-
Verifying transaction signatures is similar to personal message signature verification, except you
|
|
111
|
-
use `verifyTransactionSignature`:
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
// import SuiGrpcClient to create a network client
|
|
115
|
-
|
|
116
|
-
// create a client connected to testnet
|
|
117
|
-
const client = new SuiGrpcClient({
|
|
118
|
-
network: 'testnet',
|
|
119
|
-
baseUrl: 'https://fullnode.testnet.sui.io:443',
|
|
120
|
-
});
|
|
121
|
-
const tx = new Transaction();
|
|
122
|
-
// ... add some transactions...
|
|
123
|
-
const bytes = await tx.build({ client });
|
|
124
|
-
|
|
125
|
-
const keypair = new Ed25519Keypair();
|
|
126
|
-
const { signature } = await keypair.signTransaction(bytes);
|
|
127
|
-
|
|
128
|
-
await verifyTransactionSignature(bytes, signature, {
|
|
129
|
-
// optionally verify that the signature is valid for a specific address
|
|
130
|
-
address: keypair.toSuiAddress(),
|
|
131
|
-
});
|
|
49
|
+
const isValid = await publicKey.verifyPersonalMessage(message, signature);
|
|
132
50
|
```
|
|
133
51
|
|
|
134
|
-
|
|
52
|
+
See [Cryptography](/sui/cryptography) for the full signing and verification API, including
|
|
53
|
+
[verifying signatures](/sui/cryptography#verifying-signatures) without the original key pair.
|
|
135
54
|
|
|
136
|
-
|
|
137
|
-
SDK uses the GraphQL API to verify the signature. This works for Mainnet signatures without any
|
|
138
|
-
additional configuration.
|
|
55
|
+
## Public keys
|
|
139
56
|
|
|
140
|
-
|
|
57
|
+
Each `Keypair` has an associated `PublicKey`, which you use to verify signatures or to retrieve its
|
|
58
|
+
Sui address. Access it from a `Keypair` with `getPublicKey()`. To recreate a keypair, and therefore
|
|
59
|
+
its public key, from saved key material, reconstruct it from the secret key:
|
|
141
60
|
|
|
142
61
|
```typescript
|
|
143
62
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
url: 'https://graphql.testnet.sui.io/graphql',
|
|
147
|
-
}),
|
|
148
|
-
});
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
For some zklogin accounts, there are two valid addresses for a given set of inputs. This means you
|
|
152
|
-
might run into issues if you try to compare the address returned by `publicKey.toSuiAddress()`
|
|
153
|
-
directly with an expected address.
|
|
154
|
-
|
|
155
|
-
Instead, you can either pass in the expected address during verification, or use the
|
|
156
|
-
`publicKey.verifyAddress(address)` method:
|
|
157
|
-
|
|
158
|
-
```typescript
|
|
63
|
+
// Recreate the keypair from its secret key (Uint8Array)
|
|
64
|
+
const keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
159
65
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
address: '0x...expectedAddress',
|
|
166
|
-
});
|
|
167
|
-
// or
|
|
168
|
-
|
|
169
|
-
if (!publicKey.verifyAddress('0x...expectedAddress')) {
|
|
170
|
-
throw new Error('Signature was valid, but was signed by a different key pair');
|
|
171
|
-
}
|
|
66
|
+
// Access the public key and derive the address
|
|
67
|
+
const publicKey = keypair.getPublicKey();
|
|
68
|
+
const address = publicKey.toSuiAddress();
|
|
69
|
+
// or derive the address directly from the keypair
|
|
70
|
+
const sameAddress = keypair.toSuiAddress();
|
|
172
71
|
```
|
|
173
72
|
|
|
174
|
-
|
|
175
|
-
[
|
|
73
|
+
See
|
|
74
|
+
[Deriving a `Keypair` from a Bech32 encoded secret key](#deriving-a-keypair-from-a-bech32-encoded-secret-key)
|
|
75
|
+
for working with encoded secret-key formats.
|
|
176
76
|
|
|
177
77
|
## Deriving a key pair from a mnemonic
|
|
178
78
|
|
|
@@ -199,14 +99,16 @@ const keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
|
199
99
|
```
|
|
200
100
|
|
|
201
101
|
You can also use the `decodeSuiPrivateKey` function to decode the secret key and determine the
|
|
202
|
-
keyScheme, and get the
|
|
203
|
-
`Keypair` class.
|
|
102
|
+
keyScheme, and get the key pair's private key bytes, which you can use to instantiate the
|
|
103
|
+
appropriate `Keypair` class.
|
|
204
104
|
|
|
205
105
|
```typescript
|
|
206
106
|
|
|
107
|
+
const encoded = 'suiprivkey1qzse89atw7d3zum8ujep76d2cxmgduyuast0y9fu23xcl0mpafgkktllhyc';
|
|
207
108
|
const { scheme, secretKey } = decodeSuiPrivateKey(encoded);
|
|
208
109
|
|
|
209
110
|
// use scheme to choose the correct key pair
|
|
111
|
+
let keypair;
|
|
210
112
|
switch (scheme) {
|
|
211
113
|
case 'ED25519':
|
|
212
114
|
keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
@@ -225,16 +127,7 @@ switch (scheme) {
|
|
|
225
127
|
See [SIP-15](https://github.com/sui-foundation/sips/blob/main/sips/sip-15.md) for additional context
|
|
226
128
|
and motivation.
|
|
227
129
|
|
|
228
|
-
|
|
229
|
-
to directly derive the keypair from the secret key.
|
|
230
|
-
|
|
231
|
-
```typescript
|
|
232
|
-
const secretKey = 'suiprivkey1qzse89atw7d3zum8ujep76d2cxmgduyuast0y9fu23xcl0mpafgkktllhyc';
|
|
233
|
-
|
|
234
|
-
const keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
235
|
-
```
|
|
236
|
-
|
|
237
|
-
You can also export a keypair to a Bech32 encoded secret key using the `getSecretKey` method.
|
|
130
|
+
You can export a keypair to a Bech32 encoded secret key using the `getSecretKey` method.
|
|
238
131
|
|
|
239
132
|
```typescript
|
|
240
133
|
const secretKey = keypair.getSecretKey();
|