Skip to content
This repository was archived by the owner on Jul 30, 2025. It is now read-only.

Commit c0f839b

Browse files
Add complex example with digest
1 parent 0164705 commit c0f839b

File tree

1 file changed

+361
-3
lines changed

1 file changed

+361
-3
lines changed

apps/nextra/pages/en/build/sdks/ts-sdk/account/account-abstraction.mdx

Lines changed: 361 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ enum AbstractionAuthData has copy, drop {
6565
}
6666
```
6767

68+
**Why is the `digest` important?**
69+
70+
The `digest` is checked by the MoveVM to ensure that the signing message of the transaction being submitted is the same as the one presented in the `AbstractionAuthData`. This
71+
is important because it allows the authentication function to verify signatures with respect to the correct transaction.
72+
73+
For example, if you want to permit a public key to sign transactions on behalf of the user, you can permit the public key to sign a transaction with a specific payload.
74+
However, if a malicious user sends a signature for the correct public key but a different payload from the `digest`, the signature will not be valid.
75+
6876
**Example (Move)**
6977

7078
This example demonstrates a simple authentication logic that checks if the authenticator is equal to `"hello world"`.
@@ -99,7 +107,7 @@ const abstractedAccount = new AbstractedAccount({
99107
});
100108
```
101109

102-
## Step-by-Step Guide
110+
## Minimal Step-by-Step Guide
103111

104112
<Steps>
105113

@@ -124,7 +132,7 @@ module deployer::hello_world_authenticator {
124132
}
125133
```
126134

127-
To deploy the module, you can use the following commands from the [Aptos CLI](/en/build/cli). We assume that you already have set up a workspace with `aptos init` and
135+
To deploy the module, you can use the following commands from the [Aptos CLI](../../../../build/cli). We assume that you already have set up a workspace with `aptos init` and
128136
declared the named addresses in your `Move.toml` file.
129137

130138
```bash
@@ -260,12 +268,362 @@ console.log("Coin transfer transaction submitted! ", pendingCoinTransferTransact
260268
### 7. Conclusion
261269

262270
To verify that you have successfully sign and submitted the transaction using the abstracted account, you can use the explorer to check the transaction. If the
263-
transaction signature contains a `function_info` and `auth_data` field, it means you succesfully used account abstraction!
271+
transaction signature contains a `function_info` and `auth_data` field, it means you succesfully used account abstraction! The full E2E demo can be found [here](https://github.com/aptos-labs/aptos-ts-sdk/blob/main/examples/typescript/public_key_authenticator_account_abstraction.ts).
264272

265273
![Transaction Signature](https://i.imgur.com/HZylFnc.png)
266274

267275
</Steps>
268276

277+
## Complex Step-by-Step Guide
278+
279+
Now that you have a basic understanding of how account abstraction works, let's dive into a more complex example.
280+
281+
In this example, we will create an authenticator that allows users to permit certain public keys to sign transactions on behalf of the abstracted account.
282+
283+
<Steps>
284+
285+
### 1. Create an Authenticator module
286+
287+
We will deploy the `public_key_authenticator` module that does two things:
288+
- Allow users to permit and/or revoke public keys from signing on behalf of the user.
289+
- Allow users to authenticate on behalf of somebody else using account abstraction.
290+
291+
```move
292+
module deployer::public_key_authenticator {
293+
use std::signer;
294+
use aptos_std::smart_table::{Self, SmartTable};
295+
use aptos_std::ed25519::{
296+
Self,
297+
new_signature_from_bytes,
298+
new_unvalidated_public_key_from_bytes,
299+
unvalidated_public_key_to_bytes
300+
};
301+
use aptos_framework::bcs_stream::{Self, deserialize_u8};
302+
use aptos_framework::auth_data::{Self, AbstractionAuthData};
303+
304+
// ====== Error Codes ====== //
305+
306+
const EINVALID_PUBLIC_KEY: u64 = 0x20000;
307+
const EPUBLIC_KEY_NOT_PERMITTED: u64 = 0x20001;
308+
const EENTRY_ALREADY_EXISTS: u64 = 0x20002;
309+
const ENO_PERMISSIONS: u64 = 0x20003;
310+
const EINVALID_SIGNATURE: u64 = 0x20004;
311+
312+
// ====== Data Structures ====== //
313+
314+
struct PublicKeyPermissions has key {
315+
public_key_table: SmartTable<vector<u8>, bool>,
316+
}
317+
318+
// ====== Authenticator ====== //
319+
320+
public fun authenticate(
321+
account: signer,
322+
auth_data: AbstractionAuthData
323+
): signer acquires PublicKeyPermissions {
324+
let account_addr = signer::address_of(&account);
325+
assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
326+
let permissions = borrow_global<PublicKeyPermissions>(account_addr);
327+
328+
// Extract the public key and signature from the authenticator
329+
let authenticator = *auth_data::authenticator(&auth_data);
330+
let stream = bcs_stream::new(authenticator);
331+
let public_key = new_unvalidated_public_key_from_bytes(
332+
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
333+
);
334+
let signature = new_signature_from_bytes(
335+
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
336+
);
337+
338+
// Check if the public key is permitted
339+
assert!(smart_table::contains(&permissions.public_key_table, unvalidated_public_key_to_bytes(&public_key)), EPUBLIC_KEY_NOT_PERMITTED);
340+
341+
// Verify the signature
342+
let digest = *auth_data::digest(&auth_data);
343+
assert!(ed25519::signature_verify_strict(&signature, &public_key, digest), EINVALID_SIGNATURE);
344+
345+
account
346+
}
347+
348+
// ====== Core Functionality ====== //
349+
350+
public entry fun permit_public_key(
351+
signer: &signer,
352+
public_key: vector<u8>
353+
) acquires PublicKeyPermissions {
354+
let account_addr = signer::address_of(signer);
355+
assert!(std::vector::length(&public_key) == 32, EINVALID_PUBLIC_KEY);
356+
357+
if (!exists<PublicKeyPermissions>(account_addr)) {
358+
move_to(signer, PublicKeyPermissions {
359+
public_key_table: smart_table::new(),
360+
});
361+
};
362+
363+
let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
364+
assert!(
365+
!smart_table::contains(&permissions.public_key_table, public_key),
366+
EENTRY_ALREADY_EXISTS
367+
);
368+
369+
smart_table::add(&mut permissions.public_key_table, public_key, true);
370+
371+
}
372+
373+
public entry fun revoke_public_key(
374+
signer: &signer,
375+
public_key: vector<u8>
376+
) acquires PublicKeyPermissions {
377+
let account_addr = signer::address_of(signer);
378+
379+
assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
380+
381+
let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
382+
smart_table::remove(&mut permissions.public_key_table, public_key);
383+
}
384+
385+
}
386+
```
387+
388+
Let's break down the module...
389+
390+
**Storing Public Keys**
391+
392+
The `PublicKeyPermissions` struct is a key that contains a `SmartTable` of public keys that determines
393+
whether a public key is permitted to sign transactions on behalf of the user.
394+
395+
```move
396+
module deployer::public_key_authenticator {
397+
// ...
398+
399+
struct PublicKeyPermissions has key {
400+
public_key_table: SmartTable<vector<u8>, bool>,
401+
}
402+
403+
}
404+
```
405+
406+
**Permitting and Revoking Public Keys**
407+
408+
We define two entry functions to permit and revoke public keys. These functions are used to add and remove public keys from the `PublicKeyPermissions` struct.
409+
410+
```move
411+
module deployer::public_key_authenticator {
412+
// ...
413+
414+
public entry fun permit_public_key(
415+
signer: &signer,
416+
public_key: vector<u8>
417+
) acquires PublicKeyPermissions {
418+
let account_addr = signer::address_of(signer);
419+
assert!(std::vector::length(&public_key) == 32, EINVALID_PUBLIC_KEY);
420+
421+
if (!exists<PublicKeyPermissions>(account_addr)) {
422+
move_to(signer, PublicKeyPermissions {
423+
public_key_table: smart_table::new(),
424+
});
425+
};
426+
427+
let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
428+
assert!(
429+
!smart_table::contains(&permissions.public_key_table, public_key),
430+
EENTRY_ALREADY_EXISTS
431+
);
432+
433+
smart_table::add(&mut permissions.public_key_table, public_key, true);
434+
435+
}
436+
437+
public entry fun revoke_public_key(
438+
signer: &signer,
439+
public_key: vector<u8>
440+
) acquires PublicKeyPermissions {
441+
let account_addr = signer::address_of(signer);
442+
443+
assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
444+
445+
let permissions = borrow_global_mut<PublicKeyPermissions>(account_addr);
446+
smart_table::remove(&mut permissions.public_key_table, public_key);
447+
}
448+
}
449+
```
450+
451+
**Authenticating on behalf of somebody else**
452+
453+
The `authenticate` function is the main function that allows users to authenticate on behalf of somebody else using account abstraction. The `authenticator`
454+
will contain the **public key** and a **signature** of the user. We will verify that the public key is permitted and that the signature is valid.
455+
456+
The signature is the result of signing the `digest`. The `digest` is the sha256 hash of the **signing message** which contains information about the transaction.
457+
By signing the `digest`, we confirm that the user has approved the specific transaction that was submitted.
458+
459+
```move
460+
module deployer::public_key_authenticator {
461+
// ...
462+
463+
public fun authenticate(
464+
account: signer,
465+
auth_data: AbstractionAuthData
466+
): signer acquires PublicKeyPermissions {
467+
let account_addr = signer::address_of(&account);
468+
assert!(exists<PublicKeyPermissions>(account_addr), ENO_PERMISSIONS);
469+
let permissions = borrow_global<PublicKeyPermissions>(account_addr);
470+
471+
// Extract the public key and signature from the authenticator
472+
let authenticator = *auth_data::authenticator(&auth_data);
473+
let stream = bcs_stream::new(authenticator);
474+
let public_key = new_unvalidated_public_key_from_bytes(
475+
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
476+
);
477+
let signature = new_signature_from_bytes(
478+
bcs_stream::deserialize_vector<u8>(&mut stream, |x| deserialize_u8(x))
479+
);
480+
481+
// Check if the public key is permitted
482+
assert!(smart_table::contains(&permissions.public_key_table, unvalidated_public_key_to_bytes(&public_key)), EPUBLIC_KEY_NOT_PERMITTED);
483+
484+
// Verify the signature
485+
let digest = *auth_data::digest(&auth_data);
486+
assert!(ed25519::signature_verify_strict(&signature, &public_key, digest), EINVALID_SIGNATURE);
487+
488+
account
489+
}
490+
}
491+
```
492+
493+
To deploy the module, you can use the following commands from the [Aptos CLI](../../../../build/cli). We assume that you already have set up a workspace with `aptos init` and
494+
declared the named addresses in your `Move.toml` file.
495+
496+
```bash
497+
aptos move publish --named-addresses deployer=0x1234567890123456789012345678901234567890
498+
```
499+
500+
### 2. Setup your Environment
501+
502+
Once deployed, you can setup your environment. In this example, we will use Devnet and create an account named `alice` as the user that will be authenticated on behalf of
503+
and `bob` as the user that will be permitted to sign transactions on behalf of `alice`.
504+
```ts
505+
const DEPLOYER = "0x<public_key_authenticator_deployer>"
506+
507+
const aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
508+
509+
const alice = Account.generate();
510+
const bob = Account.generate();
511+
512+
const authenticationFunctionInfo = `${deployer}::public_key_authenticator:authenticate`;
513+
```
514+
515+
### 3. (Optional) Check if Account Abstraction is Enabled
516+
517+
Before we enable the authentication function, we can check if the account has account abstraction enabled by calling the `isAccountAbstractionEnabled` function.
518+
This will return a boolean value indicating if the account has account abstraction enabled.
519+
520+
```ts
521+
const accountAbstractionStatus = await aptos.account.isAccountAbstractionEnabled({
522+
accountAddress: alice.accountAddress,
523+
authenticationFunction,
524+
});
525+
526+
console.log("Account Abstraction status: ", accountAbstractionStatus);
527+
```
528+
529+
### 4. Enable the Authentication Function
530+
531+
Assuming that the account does not have account abstraction enabled, we need to enable the authentication function for the account. This can be done by calling
532+
the `enableAccountAbstractionTransaction` function. This creates a raw transaction that needs to be signed and submitted to the network. In this example, `alice`
533+
will be the account that will be enabled.
534+
535+
```ts
536+
const transaction = await aptos.abstraction.enableAccountAbstractionTransaction({
537+
accountAddress: alice.accountAddress,
538+
authenticationFunction,
539+
});
540+
541+
const pendingTransaction = await aptos.signAndSubmitTransaction({
542+
transaction,
543+
signer: alice.signer,
544+
});
545+
546+
await aptos.waitForTransaction({ hash: pendingTransaction.hash });
547+
548+
console.log("Account Abstraction enabled for account: ", alice.accountAddress);
549+
```
550+
551+
### 5. Permit Bob's Public Key
552+
553+
Now that we have enabled the authentication function, we can permit `bob`'s public key to sign transactions on behalf of `alice`.
554+
555+
```ts
556+
const enableBobPublicKeyTransaction = await aptos.transaction.build.simple({
557+
sender: alice.accountAddress,
558+
data: {
559+
function: `${alice.accountAddress}::public_key_authenticator::permit_public_key`,
560+
typeArguments: [],
561+
functionArguments: [bob.publicKey.toUint8Array()],
562+
},
563+
});
564+
565+
const pendingEnableBobPublicKeyTransaction = await aptos.signAndSubmitTransaction({
566+
signer: alice,
567+
transaction: enableBobPublicKeyTransaction,
568+
});
569+
570+
await aptos.waitForTransaction({ hash: pendingEnableBobPublicKeyTransaction.hash });
571+
572+
console.log(`Enable Bob's public key transaction hash: ${pendingEnableBobPublicKeyTransaction.hash}`);
573+
```
574+
575+
### 6. Create an Abstracted Account
576+
577+
Now that we have permitted `bob`'s public key, we can create an abstracted account that will be used to sign transactions on behalf of `alice`.
578+
**Notice that the `signer` function uses `bob`'s signer.**
579+
580+
```ts
581+
const abstractedAccount = new AbstractedAccount({
582+
accountAddress: alice.accountAddress,
583+
signer: (digest) => {
584+
const serializer = new Serializer();
585+
bob.publicKey.serialize(serializer);
586+
bob.sign(digest).serialize(serializer);
587+
return serializer.toUint8Array();
588+
},
589+
authenticationFunction,
590+
});
591+
```
592+
593+
### 7. Sign and Submit a Transaction using the Abstracted Account
594+
595+
Now that we have created the abstracted account, we can use it to sign transactions normally. It is important that the `sender` field in the transaction
596+
is the same as the abstracted account's address.
597+
598+
```ts
599+
const coinTransferTransaction = new aptos.transaction.build.simple({
600+
sender: abstractedAccount.accountAddress,
601+
data: {
602+
function: "0x1::coin::transfer",
603+
typeArguments: ["0x1::aptos_coin::AptosCoin"],
604+
functionArguments: [alice.accountAddress, 100],
605+
},
606+
});
607+
608+
const pendingCoinTransferTransaction = await aptos.transaction.signAndSubmitTransaction({
609+
transaction: coinTransferTransaction,
610+
signer: abstractedAccount,
611+
});
612+
613+
await aptos.waitForTransaction({ hash: pendingCoinTransferTransaction.hash });
614+
615+
console.log("Coin transfer transaction submitted! ", pendingCoinTransferTransaction.hash);
616+
```
617+
618+
### 8. Conclusion
619+
620+
To verify that you have successfully sign and submitted the transaction using the abstracted account, you can use the explorer to check the transaction. If the
621+
transaction signature contains a `function_info` and `auth_data` field, it means you succesfully used account abstraction! The full E2E demo can be found [here](https://github.com/aptos-labs/aptos-ts-sdk/blob/main/examples/typescript/public_key_authenticator_account_abstraction.ts)
622+
623+
![Transaction Signature](https://i.imgur.com/3U40YSb.png)
624+
625+
</Steps>
626+
269627
## Management Operations
270628

271629
If you want to disable account abstraction for an account, you can use the `disableAccountAbstractionTransaction`. If you do not specify an authentication function,

0 commit comments

Comments
 (0)