Pragma
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Help
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
2
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
# Starknet OS The Starknet OS is a cairo program run by SHARP to prove Starknet's state transitions. Yes, that means that Starknet itself is written in Cairo! SHARP doesn't know anything about Starknet, all it takes as an input are base64 encoded Cairo PIE (Program Independent Execution) files which are yielded by the cairo runner. The program hash of the Starknet OS is registered on the [Starknet Core Contract](https://etherscan.io/address/0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4#readProxyContract) which prevents one to prove a state transition with a different version of the OS. I've decided to write this document to walk through the Starknet OS cairo program which is not studied enough. As we are finalizing the work on SHARP integration into [Madara](https://github.com/keep-starknet-strange/madara) it is also a good opportunity to understand how one could tweak the Starknet OS for their own app chain and what would be the consequences of such changes. ## The code *The full program can be found in the [cairo-lang](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/os.cairo) repository* ![](https://hackmd.io/_uploads/BJ4jOf1ep.png) # Inputs loading As every cairo program, it all starts with a `main` entrypoint. ```cairo= // Reserve the initial range check for self validation. // Note that this must point to the first range check used by the OS. let initial_range_check_ptr = range_check_ptr; let range_check_ptr = range_check_ptr + 1; let (initial_carried_outputs: OsCarriedOutputs*) = alloc(); %{ from starkware.starknet.core.os.os_input import StarknetOsInput os_input = StarknetOsInput.load(data=program_input) ids.initial_carried_outputs.messages_to_l1 = segments.add_temp_segment() ids.initial_carried_outputs.messages_to_l2 = segments.add_temp_segment() %} ``` To begin with, the OS takes some input in the following form ```python= @marshmallow_dataclass.dataclass(frozen=True) class StarknetOsInput(ValidatedMarshmallowDataclass): contract_state_commitment_info: CommitmentInfo contract_class_commitment_info: CommitmentInfo deprecated_compiled_classes: Dict[int, DeprecatedCompiledClass] = field( metadata=fields.new_class_hash_dict_keys_metadata( values_schema=DeprecatedCompiledClass.Schema ) ) compiled_classes: Dict[int, CompiledClass] = field( metadata=fields.new_class_hash_dict_keys_metadata(values_schema=CompiledClass.Schema) ) contracts: Dict[int, ContractState] class_hash_to_compiled_class_hash: Dict[int, int] general_config: StarknetGeneralConfig transactions: List[InternalTransaction] block_hash: int ``` As we can see the input consists of the block info, contracts deployed, declared, transactions. We will see after where do these input come from. Then, we retrieve what is called the `Block Context`, we can picture this as the necessary [information](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/block_context.cairo) to execute the transactions within this block such as classes, block number/timestamp, sequencer address, chain_id, fee token address.. ```cairo= // Represents information that is the same throughout the block. struct BlockContext { // Parameters for select_builtins. builtin_params: BuiltinParams*, // A list of (compiled_class_hash, compiled_class) with the classes that are executed // in this block. n_compiled_class_facts: felt, compiled_class_facts: CompiledClassFact*, // A list of (deprecated_compiled_class_hash, deprecated_compiled_class) with // the classes that are executed in this block. n_deprecated_compiled_class_facts: felt, deprecated_compiled_class_facts: DeprecatedCompiledClassFact*, // Information about the block. block_info: BlockInfo*, // StarknetOsConfig instance. starknet_os_config: StarknetOsConfig, // A function pointer to the 'execute_syscalls' function. execute_syscalls_ptr: felt*, // A function pointer to the 'execute_deprecated_syscalls' function. execute_deprecated_syscalls_ptr: felt*, } ``` A few interesting lines we can zoom on ```cairo= block_info=new BlockInfo( block_number=nondet %{ deprecated_syscall_handler.block_info.block_number %}, block_timestamp=nondet %{ deprecated_syscall_handler.block_info.block_timestamp %}, sequencer_address=nondet %{ os_input.general_config.sequencer_address %}, ), starknet_os_config=StarknetOsConfig( chain_id=nondet %{ os_input.general_config.chain_id.value %}, fee_token_address=nondet %{ os_input.general_config.fee_token_address %}, ), ``` First, we can see the use of the keyword `nondet`, which implies some [non determinism](https://medium.com/@pban/demystifying-cairo-white-paper-part-ii-9f9dc51886e9). To quote Lior on this keyword: > "nondet" is just a way to inline a hint. In this case, it's a way to populate the OS inputs into Cairo structs. It's non-deterministic from the point of view of the verifier as the verifier doesn't know where these values come from and whether they are "correct" in any sense, until there's actual (non-hint) Cairo code that deals with them Then, we observe that some parameters are given through deprecated syscalls while others come directly from a hint variable `os_input`. It's set when loading the program input as part of the 1st hint, the program input is given to the cairo runner. The OS will then initialize the state changes ```cairo= let ( contract_state_changes: DictAccess*, contract_class_changes: DictAccess* ) = initialize_state_changes(); ``` This merely initializes some dictionaries given the fact that we know from the hint variable `os_input` which contracts were interacted with in this block. Interestingly, we also see that `dict_new` constructor seems to use the `initial_dict` scope variable under the hood. ```cairo= // Initializes state changes dictionaries. func initialize_state_changes() -> ( contract_state_changes: DictAccess*, contract_class_changes: DictAccess* ) { %{ from starkware.python.utils import from_bytes initial_dict = { address: segments.gen_arg( (from_bytes(contract.contract_hash), segments.add(), contract.nonce)) for address, contract in os_input.contracts.items() } %} // A dictionary from contract address to a dict of storage changes of type StateEntry. let (contract_state_changes: DictAccess*) = dict_new(); %{ initial_dict = os_input.class_hash_to_compiled_class_hash %} // A dictionary from class hash to compiled class hash (Casm). let (contract_class_changes: DictAccess*) = dict_new(); return ( contract_state_changes=contract_state_changes, contract_class_changes=contract_class_changes ); } ``` # Block Preprocessing Now, it's time for some pre-processing! ```cairo= // Pre-process block. with contract_state_changes { write_block_number_to_block_hash_mapping(block_context=block_context); } ``` Here is the full source code which we will explain step by step ```cairo= // Writes the hash of the (current_block_number - buffer) block under its block number in the // dedicated contract state, where buffer=STORED_BLOCK_HASH_BUFFER. func write_block_number_to_block_hash_mapping{range_check_ptr, contract_state_changes: DictAccess*}( block_context: BlockContext* ) { alloc_locals; tempvar old_block_number = block_context.block_info.block_number - STORED_BLOCK_HASH_BUFFER; let is_old_block_number_non_negative = is_nn(old_block_number); if (is_old_block_number_non_negative == FALSE) { // Not enough blocks in the system - nothing to write. return (); } // Fetch the (block number -> block hash) mapping contract state. local state_entry: StateEntry*; %{ ids.state_entry = __dict_manager.get_dict(ids.contract_state_changes)[ ids.BLOCK_HASH_CONTRACT_ADDRESS ] %} // Currently, the block hash mapping is not enforced by the OS. local old_block_hash; %{ ( old_block_number, old_block_hash ) = execution_helper.get_old_block_number_and_hash() assert old_block_number == ids.old_block_number,( "Inconsistent block number. " "The constant STORED_BLOCK_HASH_BUFFER is probably out of sync." ) ids.old_block_hash = old_block_hash %} // Update mapping. assert state_entry.class_hash = 0; assert state_entry.nonce = 0; tempvar storage_ptr = state_entry.storage_ptr; assert [storage_ptr] = DictAccess(key=old_block_number, prev_value=0, new_value=old_block_hash); let storage_ptr = storage_ptr + DictAccess.SIZE; %{ storage = execution_helper.storage_by_address[ids.BLOCK_HASH_CONTRACT_ADDRESS] storage.write(key=ids.old_block_number, value=ids.old_block_hash) %} // Update contract state. tempvar new_state_entry = new StateEntry(class_hash=0, storage_ptr=storage_ptr, nonce=0); dict_update{dict_ptr=contract_state_changes}( key=BLOCK_HASH_CONTRACT_ADDRESS, prev_value=cast(state_entry, felt), new_value=cast(new_state_entry, felt), ); return (); } ``` 1. The code starts by retrieving the `old_block_number`, this is computed by substracting the block_number of the current context (which we retrieved earlier!) and some buffer. We also learn more about the buffer meaning ```cairo= // The block number -> block hash mapping is written for the current block number minus this number. const STORED_BLOCK_HASH_BUFFER = 10; ``` This buffer is chosen such that there is enough time to do all this computation client side. In Madara, we tried to do the state root computation synchronously (at the end of every block) but that was just taking too long especially as the state was growing. With this buffer we make sure we have enough time, only drawback is that syscalls can only give you info about the block 10 blocks behind (?). 2. From the first dictionary we initialized earlier (`contract_state_changes`), we fetch the `StateEntry` storage changes dictionary as specified in the comment `// A dictionary from contract address to a dict of storage changes of type StateEntry.` Here we learn that 1 contracts address is reserved for the OS ([source](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/constants.cairo#L47C10-L47C10)): ```cairo= // OS reserved contract addresses. // This contract stores the block number -> block hash mapping. const BLOCK_HASH_CONTRACT_ADDRESS = 1; ``` This contract is not *deployed* per se but given as part of the input. As per lior, it's not an actual contract, it's just a storage that contains the block hashes `block[i] = block_hash`. 3. Let's then introduce the `execution_helper` hint variable. As the python code [states](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/syscall_handler.py#L1038) its role is to maintain the information needed for executing transactions in the OS. In this hint, we use it to fetch the old block number and hash. ```python= def get_old_block_number_and_hash(self) -> Tuple[int, int]: """ Returns the block number and block hash of the (current_block_number - stored_block_hash_buffer) block. """ assert ( self.old_block_number_and_hash is not None ), f"Block number is probably < {constants.STORED_BLOCK_HASH_BUFFER}." return self.old_block_number_and_hash ``` Here again, we find our buffer constant, in fact it will only return a value if we are at a block past that buffer. If we look closely at the [create](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/syscall_handler.py#L1098C13-L1098C13) method from the `OsExecutionHelper`, we see that it fetches this information from the updated state which was written by the `Batcher`. Finally `assert old_block_number == ids.old_block_number` makes sure that the `old_block_hash` that was returned by `get_old_block_number_and_hash` is in fact the `current_block - STORED_BLOCK_HASH_BUFFER`. 4. The last part of the function is quite straightforward, we need to update the content of our `StateEntry` dict such that it reflects the storage change from our block_number to block_hash mapping inside our contract (you know the one reserved for the OS). We again ask some help to the `OsExecutionHelper` hint variable to write in the storage : ```cairo= storage = execution_helper.storage_by_address[ids.BLOCK_HASH_CONTRACT_ADDRESS] storage.write(key=ids.old_block_number, value=ids.old_block_hash) ``` Specifically, we get the storage of our *super* contract thanks to the `Mapping[int, OsSingleStarknetStorage]`. This class represents a single contract storage and it is used by the Starknet OS run in the GpsAmbassador. It also exposes [read/write](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/storage/starknet_storage.py#L158) functions to ongoing storage changes. As per Lior, the GpsAmbassador is the service responsible for running the OS and sending the run to SHARP to be proven. In Madara this refers to the sharp worker run in a separate thread in the node. 5. Last step, we update our `contract_state_changes` dictionary with our new `StateEntry` constructed with our new storage! # Transactions Execution After we've pre-processed the block and updated our block number -> block hash mapping, we can execute the transactions within the block. ```cairo= // Execute transactions. let outputs = initial_carried_outputs; with contract_state_changes, contract_class_changes, outputs { let (local reserved_range_checks_end) = execute_transactions(block_context=block_context); } let final_carried_outputs = outputs; ``` Let's dive into the internal of the `execute_transactions` [function](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/os.cairo#L18). If you're familiar with the [blockifier](https://github.com/starkware-libs/blockifier), you will see some similar logic but written in Cairo instead of Rust. The comments help us understand the global flow ```cairo= // Executes the transactions in the hint variable os_input.transactions. // // Returns: // reserved_range_checks_end - end pointer for the reserved range checks. // // Assumptions: // The caller verifies that the memory range [range_check_ptr, reserved_range_checks_end) // corresponds to valid range check instances. // Note that if the assumption above does not hold it might be the case that // the returned range_check_ptr is smaller then reserved_range_checks_end. ``` Q: Add details (?) What's the most interesting for us is the `execute_transactions_inner` [function](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/core/os/execution/execute_transactions.cairo#L164C6-L164C32). Given a block context and a number of transactions it will execute them sequentially. ```cairo= // Guess the current transaction's type. %{ tx = next(transactions) tx_type_bytes = tx.tx_type.name.encode("ascii") ids.tx_type = int.from_bytes(tx_type_bytes, "big") %} ``` This first hint is used to retrieve the type of the transaction (declare/deploy/invoke/l1_handler). Then the OS will call the function corresponding to the transaction's type e.g `execute_invoke_function_transaction`. We will only dive into the `invoke` function but feel free to explore the code by yourself to understand the other entrypoints. We can do the parallel with the blockifier [code](https://github.com/starkware-libs/blockifier/blob/main/crates/blockifier/src/transaction/account_transaction.rs#L711) which follows the same flow as the OS. 1. Validate Transaction Version 2. Compute Transaction Hash 3. Check and increment nonce 4. Run `__validate__` entrypoint responsible for signature verification 5. Execute only non-reverted transactions 6. Charge fees Right before we run the transaction validation, we find this mysterious hint ```cairo= %{ tx_info_ptr = ids.tx_execution_context.deprecated_tx_info.address_ execution_helper.start_tx(tx_info_ptr=tx_info_ptr) %} ``` This is described in the python comments ```python= """ Called when starting the execution of a transaction. 'tx_info_ptr' is a pointer to the TxInfo struct corresponding to said transaction. """ ``` In simpler words, it just tells the OS where to look for the transaction info during execution. ```cairo= struct TxInfo { // The version of the transaction. It is fixed (currently, 1) in the OS, and should be // signed by the account contract. // This field allows invalidating old transactions, whenever the meaning of the other // transaction fields is changed (in the OS). version: felt, // The account contract from which this transaction originates. account_contract_address: felt, // The max_fee field of the transaction. max_fee: felt, // The signature of the transaction. signature_len: felt, signature: felt*, // The hash of the transaction. transaction_hash: felt, // The identifier of the chain. // This field can be used to prevent replay of testnet transactions on mainnet. chain_id: felt, // The transaction's nonce. nonce: felt, } ``` # VM Scope Before we look at the code responsible for computing state updates and commitments. I wanted to highlight one specific hint used throughout the OS. `%{ vm_enter_scope(...) %}` and `%{ vm_exit_scope() %}` It has been implemented within the rust Cairo VM [here](https://github.com/lambdaclass/cairo-vm/blob/main/vm/src/types/exec_scope.rs#L13) which we can probably leverage. It's a very simple struct that just holds `scopes` defined as `HashMap<String, Box<dyn Any>>`. Within one scope you can access the local variables and the variables from upper scopes. # State Update ```cairo= %{ # This hint shouldn't be whitelisted. vm_enter_scope(dict( commitment_info_by_address=execution_helper.compute_storage_commitments(), os_input=os_input, )) ids.initial_state_updates_ptr = segments.add_temp_segment() %} ``` This hint creates the scope that will be used by the next hint, mainly we make accessible to the vm the os input and the storage commitments. ```cairo= let state_updates_ptr = initial_state_updates_ptr; with state_updates_ptr { let (state_update_output) = state_update{hash_ptr=pedersen_ptr}( contract_state_changes_start=contract_state_changes_start, contract_state_changes_end=contract_state_changes, contract_class_changes_start=contract_class_changes_start, contract_class_changes_end=contract_class_changes, ); } ``` The `state_update` function does a few things but mostly it updates the different commitment trees which will be used to update and validate the global state. It outputs both the `initial_root` and the `final_root`. For more information about how Starknet stores its own state please refer to the official [documentation](https://docs.starknet.io/documentation/architecture_and_concepts/State/starknet-state/). The state update is performed in 3 main steps: 1. Updates contract state trie: `contract_state_update` 2. Updates contract class trie: `contract_class_update` 3. Compute the initial and final roots of the global state: `calculate_global_state_root` Q: Why is the initial root not passed through a hint ? The 2 first steps are quite similar so we'll only take a close look at the `contract_state_update`. The function starts by setting up everything so the merkle trie updates can happen smoothly. Mostly doing some dictionary allocation, hashing the state changes and storing the number of actual state changes. The main hint of this function is the following: ```cairo= %{ ids.initial_root = os_input.contract_state_commitment_info.previous_root ids.final_root = os_input.contract_state_commitment_info.updated_root preimage = { int(root): children for root, children in os_input.contract_state_commitment_info.commitment_facts.items() } assert os_input.contract_state_commitment_info.tree_height == ids.MERKLE_HEIGHT %} ``` We set the initial and final roots by retrieving the state commitments from the input (remember the beginning). Then we set the preimage by looking at the commitment facts, this preimage will then be used by the [merkle_multi_update](https://github.com/starkware-libs/cairo-lang/blob/27a157d761ae49b242026bcbe5fca6e60c1e98bd/src/starkware/cairo/common/merkle_multi_update.cairo#L68C15-L68C15) function. Finally we set the `MERKLE_HEIGHT` value of the outer scope to the correct trie height (251). Now that we have everything, we can perform the actual trie update ```cairo= patricia_update_using_update_constants( patricia_update_constants=patricia_update_constants, update_ptr=hashed_state_changes, n_updates=n_contract_state_changes, height=MERKLE_HEIGHT, prev_root=initial_root, new_root=final_root, ); ``` One interesting trick used here is to pass the powers of 2 as [pre-computed](https://github.com/starkware-libs/cairo-lang/blob/27a157d761ae49b242026bcbe5fca6e60c1e98bd/src/starkware/cairo/common/patricia.cairo#L418) constants to the function which saves us a ton of computation. # Merkle Patricia Trie As we've seen in the previous section, the Starknet OS uses merkle patricia tries with pedersen or poseidon depending on the trie. ```cairo= from starkware.cairo.common.patricia import ( patricia_update_constants_new, patricia_update_using_update_constants, ) from starkware.cairo.common.patricia_utils import PatriciaUpdateConstants from starkware.cairo.common.patricia_with_poseidon import ( patricia_update_using_update_constants as patricia_update_using_update_constants_with_poseidon, ) ``` For instance let's look at the first major hint ```cairo= %{ from starkware.cairo.common.patricia_utils import canonic, patricia_guess_descents from starkware.python.merkle_tree import build_update_tree # Build modifications list. modifications = [] DictAccess_key = ids.DictAccess.key DictAccess_new_value = ids.DictAccess.new_value DictAccess_SIZE = ids.DictAccess.SIZE for i in range(ids.n_updates): curr_update_ptr = ids.update_ptr.address_ + i * DictAccess_SIZE modifications.append(( memory[curr_update_ptr + DictAccess_key], memory[curr_update_ptr + DictAccess_new_value])) node = build_update_tree(ids.height, modifications) descent_map = patricia_guess_descents( ids.height, node, preimage, ids.prev_root, ids.new_root) del modifications __patricia_skip_validation_runner = globals().get( '__patricia_skip_validation_runner') common_args = dict( preimage=preimage, descent_map=descent_map, __patricia_skip_validation_runner=__patricia_skip_validation_runner) common_args['common_args'] = common_args %} ``` This shouldn't be necessary difficult as it ends up mostly translating python code to rust and we can probably gain performance from this too. However it will need extensive testing for each of these against their respective python implementations which are battle-tested. # Conclusion This post is still a draft and doesn't aim to be fully exhaustive. It's here to give an overview in the inner workings of the Starknet OS and help during the development of the [SNOS](https://github.com/keep-starknet-strange/snos) rust library. Here are a few interesting tweaks that could ba applied to the OS in the context of an app-chain: - **Blocks batching**: as we've seen the OS cairo program operates on one block. In order to support low block time app-chains it should run on multiple blocks at once. In that case, we would only pay for state updates between the initial and final state. - **State Representation**: the OS uses merkle patricia tries to represent the different state tries. If you're building a permissionned app-chain or have a limited state growth then it could be interesting to change these to simpler and more efficient binary tries. Poseidon should also be used everywhere probably. - **Fee Model**: the fee charging logic could be changed to integrate a paymaster protocol at the OS level and thus making the whole chain gas-less. There are probably a ton of other changes that could be made to tailor the Starknet OS to your specific use case. If you have ideas please create an issue on the [SNOS](https://github.com/keep-starknet-strange/snos) repository. ### Credits Thank you to everyone contributing and reviewing this document. > Lior Goldberg from Starkware, Ben from Starkware, Abdel from Starkware, Apoorv from Pragma.

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully