This commit is contained in:
Corwin Smith 2022-11-09 14:32:37 -07:00
parent d7a2af0cef
commit 99dfffb820
11 changed files with 125 additions and 61 deletions

View File

@ -22,9 +22,7 @@ export const Header: FC = () => {
>
<NextLink href={'/'} passHref>
<Link _hover={{ textDecoration: 'none' }}>
<Text textStyle='header-font'>
go-ethereum
</Text>
<Text textStyle='header-font'>go-ethereum</Text>
</Link>
</NextLink>
</Stack>

View File

@ -40,7 +40,13 @@ export const DownloadsHero: FC<DownloadsHero> = ({
DOWNLOAD_HEADER_BUTTONS.sourceCode.buildURL = sourceCodeURL;
return (
<Grid border='3px solid' borderColor='brand.light.primary' p={4} templateColumns={{ base: 'repeat(1, 1fr)', lg: '1fr 430px' }} gap={4}>
<Grid
border='3px solid'
borderColor='brand.light.primary'
p={4}
templateColumns={{ base: 'repeat(1, 1fr)', lg: '1fr 430px' }}
gap={4}
>
<Flex flexDirection='column'>
<Flex mb={4} direction={{ base: 'column', md: 'row-reverse' }}>
<Flex

View File

@ -93,7 +93,6 @@ Return:
The following tracers are implement in Go. This means they are much more performant than other tracers that are written in Javascript. The tracers are selected by passing their name to the `tracer` parameter when invoking a tracing API method, e.g. `debug.traceTransaction(<txhash>, { tracer: 'callTracer' })`.
### 4byteTracer
Solidity contract functions are [addressed](https://docs.soliditylang.org/en/develop/abi-spec.html#function-selector) using the first four four byte of the Keccak-256 hash of their signature. Therefore when calling the function of a contract, the caller must send this function selector as well as the ABI-encoded arguments as call data.
@ -135,7 +134,6 @@ The `callTracer` tracks all the call frames executed during a transaction, inclu
| error | string | error, if any |
| calls | []callframe | list of sub-calls |
Example Call:
```sh
@ -178,7 +176,6 @@ Things to note about the call tracer:
- Calls to precompiles are also included in the result
- In case a frame reverts, the field `output` will contain the raw return data, unlike [revertReasonTracer](#revertreasontracer) which parses the data and returns the revert message
### prestateTracer
The prestate tracer has two modes: `prestate` and `diff`. The `prestate` mode returns the accounts necessary to execute a given transaction. `diff` mode returns the differences between the transaction's pre and post-state (i.e. what changed because the transaction happened). The `prestateTracer` defaults to `prestate` mode. It reexecutes the given transaction and tracks every part of state that is touched. This is similar to the concept of a [stateless witness](https://ethresear.ch/t/the-stateless-client-concept/172), the difference being this tracer doesn't return any cryptographic proof, rather only the trie leaves. The result is an object. The keys are addresses of accounts. The value is an object with the following fields:
@ -195,7 +192,15 @@ To run this tracer in `diff` mode, pass `tracerConfig: {diffMode: true}` in the
Example:
```js
debug.traceCall({from: "0x35a9f94af726f07b5162df7e828cc9dc8439e7d0", to: "0xc8ba32cab1757528daf49033e3673fae77dcf05d", data: "0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000"}, 'latest', {tracer: 'prestateTracer'})
debug.traceCall(
{
from: '0x35a9f94af726f07b5162df7e828cc9dc8439e7d0',
to: '0xc8ba32cab1757528daf49033e3673fae77dcf05d',
data: '0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000'
},
'latest',
{ tracer: 'prestateTracer' }
);
```
Return:
@ -244,7 +249,6 @@ Return (same call with `{diffMode: True}`):
}
```
### revertReasonTracer
The `revertReasonTracer` is useful for analyzing failed transactions. If the transaction reverted, the reason for the revert (according to the Solidity contract) is returned. For any other failure, the error message is returned.
@ -265,7 +269,6 @@ Returns:
This tracer is noop. It returns an empty object and is only meant for testing the setup.
## Javascript tracers
There are also a set of tracers written in Javascript. These are less performant than the Go native tracers because of overheads associated with interpreting the Javascript in Geth's Go environment.
@ -277,7 +280,15 @@ There are also a set of tracers written in Javascript. These are less performant
Example:
```js
debug.traceCall({from: "0x35a9f94af726f07b5162df7e828cc9dc8439e7d0", to: "0xc8ba32cab1757528daf49033e3673fae77dcf05d", data: "0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000"}, 'latest', {tracer: 'bigramTracer'})
debug.traceCall(
{
from: '0x35a9f94af726f07b5162df7e828cc9dc8439e7d0',
to: '0xc8ba32cab1757528daf49033e3673fae77dcf05d',
data: '0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000'
},
'latest',
{ tracer: 'bigramTracer' }
);
```
Returns:
@ -371,7 +382,15 @@ Returns:
Example:
```js
debug.traceCall({from: "0x35a9f94af726f07b5162df7e828cc9dc8439e7d0", to: "0xc8ba32cab1757528daf49033e3673fae77dcf05d", data: "0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000"}, 'latest', {tracer: 'opcountTracer'})
debug.traceCall(
{
from: '0x35a9f94af726f07b5162df7e828cc9dc8439e7d0',
to: '0xc8ba32cab1757528daf49033e3673fae77dcf05d',
data: '0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000'
},
'latest',
{ tracer: 'opcountTracer' }
);
```
Returns:
@ -381,15 +400,25 @@ Returns:
```
### trigram
`trigramTracer` counts the opcode trigrams. Trigrams are the possible combinations of three opcodes this tracer reports how many times each combination is seen during execution.
Example:
```js
debug.traceCall({from: "0x35a9f94af726f07b5162df7e828cc9dc8439e7d0", to: "0xc8ba32cab1757528daf49033e3673fae77dcf05d", data: "0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000"}, 'latest', {tracer: 'trigramTracer'})
debug.traceCall(
{
from: '0x35a9f94af726f07b5162df7e828cc9dc8439e7d0',
to: '0xc8ba32cab1757528daf49033e3673fae77dcf05d',
data: '0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000'
},
'latest',
{ tracer: 'trigramTracer' }
);
```
Returns:
```terminal
{
--PUSH1: 1,
@ -414,17 +443,18 @@ Returns:
}
```
### unigram
`unigramTracer` counts the frequency of occurrance of each opcode.
Example:
```js
> debug.traceCall({from: "0x35a9f94af726f07b5162df7e828cc9dc8439e7d0", to: "0xc8ba32cab1757528daf49033e3673fae77dcf05d", data: "0xd1a2eab2000000000000000000000000000000000000000000000000000000000024aea100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000050000000204895cd480cc8412691a880028a25aec86786f1ed2aa5562bc400000000000000c6403c14f35be1da6f433eadbb6e9178a47fbc7c6c1d568d2f2b876e929089c8d8db646304fd001a187dc8a600000000000000000000000000000000"}, 'latest', {tracer: 'unigramTracer'})
```
Returns:
```terminal
{
ADD: 36,
@ -447,8 +477,7 @@ Returns:
## State overrides
It is possible to give temporary state modifications to Geth in order to simulate the effects of `eth_call`. For example, some new byetcode could be deployed to some address *temporarily just for the duration of the execution* and then a transaction interacting with that address canm be traced. This can be used for scenario testing or determining the outcome of some hypothetical transaction before executing for real.
It is possible to give temporary state modifications to Geth in order to simulate the effects of `eth_call`. For example, some new byetcode could be deployed to some address _temporarily just for the duration of the execution_ and then a transaction interacting with that address canm be traced. This can be used for scenario testing or determining the outcome of some hypothetical transaction before executing for real.
To do this, the tracer is written as normal, but the parameter `stateOverrides` is passed an address and some bytecode.

View File

@ -4,8 +4,10 @@ description: Introduction to tracing EVM transactions using Geth
---
---
title: EVM Tracing
sort-key: A
---
Tracing allows users to examine precisely what was executed by the EVM during some specific transaction or set of transactions. There are two different types of [transactions](https://ethereum.org/en/developers/docs/transactions) in Ethereum: value transfers and contract executions. A value transfer just moves ETH from one account to another. A contract interaction executes some code stored at a contract address which can include altering stored data and transacting multiple times with other contracts and externally-owned accounts. A contract execution transaction can therefore be a complicated web of interactions that can be difficult to unpick. The transaction receipt contains a status code that shows whether the transaction succeeded or failed, but more detailed information is not readily available, meaning it is very difficult to know what a contract execution actually did, what data was modified and which addresses were touched. This is the problem that EVM tracing solves. Geth traces transactions by re-running them locally and collecting data about precisely what was executed by the EVM.
@ -34,12 +36,11 @@ This means there are limits on the transactions that can be traced imposed by th
![state pruning options](/public/images/state-pruning.png)
*This image shows the state stored by each sync-mode - red indicates stored state. The full width of each line represents origin to present head*
_This image shows the state stored by each sync-mode - red indicates stored state. The full width of each line represents origin to present head_
More detailed information about syncing is available on the [sync modes page](/pages/docs/fundamentals/sync-modes.md).
When a trace of a specific transaction is executed, the state is prepared by fetching the state of the parent block from the database. If it is not available, Geth will crawl backwards in time to find the next available state but only up to a limit defined in the `reexec` parameter which defaults to 128 blocks. If no state is available within the `reexec` window then the trace fails with `Error: required historical state unavailable` and the `reexec` parameter must be increased. If a valid state *is* found in the `reexec` window, then Geth sequentially re-executes the transcations in each block between the last available state and the target block. The greater the value of `reexec` the longer the tracing will take because more blocks have to be re-executed to regenerate the target state.
When a trace of a specific transaction is executed, the state is prepared by fetching the state of the parent block from the database. If it is not available, Geth will crawl backwards in time to find the next available state but only up to a limit defined in the `reexec` parameter which defaults to 128 blocks. If no state is available within the `reexec` window then the trace fails with `Error: required historical state unavailable` and the `reexec` parameter must be increased. If a valid state _is_ found in the `reexec` window, then Geth sequentially re-executes the transcations in each block between the last available state and the target block. The greater the value of `reexec` the longer the tracing will take because more blocks have to be re-executed to regenerate the target state.
The `debug_getAccessibleStates` endpoint is a useful tool for estimating a suitable value for `reexec`. Passing the number of the block that contains the target transaction and a search distance to this endpoint will return the number of blocks behind the current head where the most recent available state exists. This value can be passed to the tracer as `re-exec`.
@ -58,7 +59,6 @@ More information about Geth's built-in tracers is available on the
[built-in tracers](/pages/docs/developers/evm-tracing/built-in-tracers.md)
page.
### Custom tracers
In addition to built-in tracers, it is possible to provide custom code that hooks to events in the EVM to process and return data in a consumable format. Custom tracers can be written either in Javascript or Go. JS tracers are good for quick prototyping and experimentation as well as for less intensive applications. Go tracers are performant but require the tracer to be compiled together with the Geth source code. This means developers only have to gather the data they actually need, and do any processing at the source.

View File

@ -84,6 +84,7 @@ created by github.com/ethereum/go-ethereum/p2p.newDialScheduler
```
and
```sh
debug.stacks("consolecmd.go")
```

View File

@ -50,7 +50,6 @@ geth --sepolia --datadir sepolia <other flags> --signer=sepolia-data/clef/clef.i
Remember that it is also necessary to have a consensus client running too, which requires `--http` and several `authrpc` values to be provided to Geth. A complete set of startup commands for the Geth-Lodestar client combinaton plus Clef is provided as an example in this [Gist](https://gist.github.com/jmcook1186/ea5de9215ecedb1b0105bcfa9c30d44c) - adapt it for different client combinations and configurations.
## Interacting with Clef
There are two modes of interaction with Clef. One is direct interaction, which is achieved by passing requests by HTTP or IPC with JSON-RPC data as defined in Clef's external API. This is the way to do things in Clef that don't require Geth, such as creating and listing accounts, or signing data offline. The other way is via Geth. With Geth started with Clef as an external signer, requests made to Geth that touch account data will route via Clef for approval. By default, the user approves or denies interactions manually by typing `y` or `n` into the Clef console when prompted, but custom rules can also be created to automate common tasks.
@ -70,6 +69,7 @@ The same can be achieved using raw JSON requests (this example send the request
```shell
curl -X POST --data '{"id": 0, "jsonrpc": "2.0", "method": "account_new", "params": []}' http://localhost:8550 -H "Content-Type: application/json"
```
The console will hang because Clef is waiting for manual approval. Switch to the Clef terminal and approve the action. Clef will prompt for a account password and then confirm the account creation in the terminal logs. A new keyfile has been added to the keystore in `go-ethereum/sepolia-data`. A JSON response is returned to the terminal the request originated from, containing the new account address in the `result` field.
```terminal
@ -84,7 +84,6 @@ The newly generated key files can be viewed in `<datadir>/keystore/`. The file n
`UTC--2022-05-19T12-34-36.47413510Z--0b85e5a13e118466159b1e1b6a4234e5f9f784bb`
An account can also be created by importing a raw private key (hex string) using `clef importraw` as follows:
```sh
@ -105,7 +104,6 @@ access to the key and all associated funds!
Make sure to backup keystore and passwords in a safe location.
```
### Listing accounts
The accounts in the keystore can be listed to the terminal using a simple CLI command as follows:
@ -216,7 +214,6 @@ Updating the account using `geth account update` replaces the original file with
With Clef, indiscriminate account unlocking is no longer a feature. Instead, Clef unlocks are locked until actions are explicitly approved manually by a user, unless they conform to some specific scenario that has been encoded in a ruleset. Please refer to our Clef docs for instructions for how to create rulesets.
### Transactions
Transactions can be sent using raw JSON requests to Geth or using `web3js` in the Javascript console. Either way, with Clef acting as the signer the transactions will not get sent until approval is given in Clef. The following code snippet shows how a transaction could be sent between two accounts in the keystore using the Javascript console.

View File

@ -4,6 +4,7 @@ description: Introduction to how Ethereum nodes are organized and where Geth fit
---
## Node architecture
An Ethereum node is composed of two clients: an [execution client](https://ethereum.org/en/developers/docs/nodes-and-clients/#execution-clients) and a [consensus client](https://ethereum.org/en/developers/docs/nodes-and-clients/#consensus-clients). Geth is an [execution client](https://ethereum.org/en/developers/docs/nodes-and-clients/#execution-clients). Originally, an execution client alone was enough to run a full Ethereum node. However, ever since Ethereum turned off [proof-of-work](https://ethereum.org/en/developers/docs/consensus-mechanisms/pow/) and implemented [proof-of-stake](https://ethereum.org/en/developers/docs/consensus-mechanisms/pow/), Geth has needed to be coupled to another piece of software called a [“consensus client”](https://ethereum.org/en/developers/docs/nodes-and-clients/#consensus-clients) in order to keep track of the Ethereum blockchain.
The execution client (Geth) is responsible for transaction handling, transaction gossip, state management and supporting the Ethereum Virtual Machine ([EVM])(https://ethereum.org/en/developers/docs/evm/). However, Geth is **not** responsible for block building, block gossiping or handling consensus logic. These are in the remit of the consensus client.
@ -29,4 +30,3 @@ The consensus client deals with all the logic that enables a node to stay in syn
Validators can be added to consensus clients if 32 ETH have been sent to the deposit contract. The validator client comes bundled with the consensus client and can be added to a node at any time. The validator handles attestations and block proposals. They enable a node to accrue rewards or lose ETH via penalties or slashing. Running the validator software also makes a node eligible to be selected to propose a new block.
Read more about [proof-of-stake](https://ethereum.org/en/developers/docs/consensus-mechanisms/pos/).

View File

@ -53,28 +53,44 @@ These are all the data required to deploy the contract using the Geth Javascript
Now, for convenice we can store the abi and bytecode in variables in the console:
```js
var abi = [{"inputs":[],"name":"retrieve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"number","type":"uint256"}],"name":"store","outputs":[],"stateMutability":"nonpayable","type":"function"}]
var abi = [
{
inputs: [],
name: 'retrieve',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function'
},
{
inputs: [{ internalType: 'uint256', name: 'number', type: 'uint256' }],
name: 'store',
outputs: [],
stateMutability: 'nonpayable',
type: 'function'
}
];
var bytecode = "608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea264697066735822122031443f2fb748bdb27e539fdbeb0c6f575aec50508baaa7e4dbeb08577ef19b3764736f6c63430008110033"
var bytecode =
'608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea264697066735822122031443f2fb748bdb27e539fdbeb0c6f575aec50508baaa7e4dbeb08577ef19b3764736f6c63430008110033';
```
The ABI can be used to create an instance of the contract:
```js
var contract = eth.contract(abi)
var contract = eth.contract(abi);
```
This contract instance can then be deployed to the blockchain. This is done using `eth.sendTransaction`, passing the contract bytecode in the `data` field. For convenience we can create a transaction JSON object first, then pass it to `eth.sendTransaction` later. Let's use the first account in `eth.accounts` as the sender. The amount of gas to include can be determined using `eth.estimateGas`:
```js
var gas = eth.estimateGas({data: bytecode})
var gas = eth.estimateGas({ data: bytecode });
```
Note that each command that touches accounts will require **approval in Clef** unless a custom rule has been implemented. The bytecode, gas and address of the sender can be bundled together into an object that will be passed to the contract's `new()` method which deploys the contract.
```js
var tx = {'from': eth.accounts[0], data: bytecode, gas: gas}
var deployed_contract = contract.new(tx)
var tx = { from: eth.accounts[0], data: bytecode, gas: gas };
var deployed_contract = contract.new(tx);
```
The transaction hash and deployment address can now been viewed in the console by entering the variable name (in this case `deployed_contract`):
@ -105,14 +121,13 @@ The transaction hash and deployment address can now been viewed in the console b
Passing the transaction hash to `eth.getTransaction()` returns the deployment transaction details including the contract bytecode in the `input` field. To interact with the contract, create an instance by passing the deployment address to `contract.at` then call the methods.
```js
var instance = contract.at("0x2d6505f8b1130a22a5998cd31788bf6c751247f")
var instance = contract.at('0x2d6505f8b1130a22a5998cd31788bf6c751247f');
// store() alters the state and therefore requires sendTransaction()
contract.set.sendTransaction(42, {from: eth.accounts[0], gas: 1000000})
contract.set.sendTransaction(42, { from: eth.accounts[0], gas: 1000000 });
// retrieve does not alter state so it can be executed using call()
contract.retrieve().call()
>> 2
contract.retrieve().call() >> 2;
```
## Summary
This page demonstrated how to create, compile, deploy and interact with an Ethereum smart contract using Geth's Javascript console.

View File

@ -216,11 +216,17 @@ const DownloadsPage: NextPage = () => {
</DownloadsSection>
<DownloadsSection id='importingkeys' sectionTitle='Importing keys and verifying builds'>
<Flex p={4} borderBottom='2px solid' borderColor='brand.light.primary' gap={4} flexDirection={{ base: 'column', md: 'row' }}>
<Flex
p={4}
borderBottom='2px solid'
borderColor='brand.light.primary'
gap={4}
flexDirection={{ base: 'column', md: 'row' }}
>
<Stack flex={1}>
<Text textStyle='quick-link-text'>
You can import the build server public keys by grabbing the individual keys directly
from the keyserver network:
You can import the build server public keys by grabbing the individual keys
directly from the keyserver network:
</Text>
</Stack>
@ -230,7 +236,13 @@ const DownloadsPage: NextPage = () => {
</Stack>
</Flex>
<Flex p={4} borderBottom='2px solid' borderColor='brand.light.primary' gap={4} flexDirection={{ base: 'column', md: 'row' }}>
<Flex
p={4}
borderBottom='2px solid'
borderColor='brand.light.primary'
gap={4}
flexDirection={{ base: 'column', md: 'row' }}
>
<Stack flex={1}>
<Text textStyle='quick-link-text'>
Similarly you can import all the developer public keys by grabbing them directly
@ -244,7 +256,13 @@ const DownloadsPage: NextPage = () => {
</Stack>
</Flex>
<Flex p={4} borderBottom='2px solid' borderColor='brand.light.primary' gap={4} flexDirection={{ base: 'column', md: 'row' }}>
<Flex
p={4}
borderBottom='2px solid'
borderColor='brand.light.primary'
gap={4}
flexDirection={{ base: 'column', md: 'row' }}
>
<Stack flex={1}>
<Text textStyle='quick-link-text'>
From the download listings above you should see a link both to the downloadable