Skip to content

feat(bridge-signer): implement FROST threshold signer node#1960

Closed
noot wants to merge 117 commits intomainfrom
noot/frost-participant
Closed

feat(bridge-signer): implement FROST threshold signer node#1960
noot wants to merge 117 commits intomainfrom
noot/frost-participant

Conversation

@noot
Copy link
Contributor

@noot noot commented Feb 10, 2025

Summary

implement FROST threshold signer node which implements the FrostParticipantService gRPC service defined in #1948. the bridge withdrawer can then collect signature partials from various threshold signer nodes to sign a withdrawal transaction in a more distributed manner.

Background

improves security of the withdrawer over having one private key used; instead, we can employ an m-of-n threshold signature scheme to sign withdrawals.

Changes

  • implement FROST threshold signer node which implements the FrostParticipantService gRPC service
  • currently, the signer is evm-rollup specific; when it receives a sequencer withdrawal tx to sign, it verifies that the withdrawals are valid using an evm rollup node. specifically, it performs the same logic that the withdrawer uses to create the withdrawal tx in the first place (one tx = every withdrawal in an evm block converted to an action) to verify.

Testing

tested with the withdrawer in #1948; see https://www.notion.so/astria-org/bridge-threshold-withdrawer-testing-1936bd31a90c803ab33ccc1221f545a1?pvs=4

Changelogs

Changelog created.

Related Issues

closes #1937

@github-actions github-actions bot added the sequencer pertaining to the astria-sequencer crate label Feb 10, 2025
github-merge-queue bot pushed a commit that referenced this pull request Mar 31, 2025
## Summary
Fixes incorrect comments related to signing keys in the sequencer app
test utils.

## Background
These changes were moved from #1960 into a separate PR.

## Changes
- Corrects which addresses signing keys correspond to in
`sequencer::app::test_utils`.

## Testing
No testing required.

Co-authored-by: noot <elizabeth@astria.org>
github-merge-queue bot pushed a commit that referenced this pull request Apr 11, 2025
## Summary
Fixes various typos in bridge withdrawer.

## Background
The changes were moved from #1960 into a separate PR.

## Changes
- Changes various instances of "sequencing" to "sequencer".

## Testing
No testing required.

## Changelogs
No updates required.

Co-authored-by: noot <elizabeth@astria.org>
github-merge-queue bot pushed a commit that referenced this pull request Apr 15, 2025
)

## Summary
Adds optional bridge withdrawer address argument to the init bridge
account command.

## Background
These changes were moved from #1960 into a separate PR.

## Changes
- Added optional bridge withdrawer address argument to the init bridge
account command.
- Opened corresponding docs PR here:
astriaorg/docs#123.

## Testing
Manually tested.

## Changelogs
Changelog updated

---------

Co-authored-by: Richard Janis Goldschmidt <github@aberrat.io>
## Summary
adds helm chart for the new bridge signer binary

## Changes
- adds new chart for bridge

## Testing
Tested locally by running the local cluster with `frostThresholdSigning:
true`, and by following documentations in #1948.
to deploy test threshold signers run `just deploy bridge-signers`. This
deploy two signers with appropriate keys.

A future task should consider adding a smoke test for the Frost signer
using the Helm chart introduced in this PR.
## Changelogs
No updates required.

## Breaking Changelist
- changes bridge-withdrawer appName templating, no longer uses the
global entry.
Ok(metrics_and_guard) => metrics_and_guard,
};

info!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is happening outside of root span (we probably have that issue in all our services). I suggest to create a freestanding, instrumented function like fn init_service, where you can emit the config and which returns the BridgeSigner.

};

if let Err(e) = bridge_signer.run_until_stopped().await {
eprintln!("bridge signer failed: {e}");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
eprintln!("bridge signer failed: {e}");
eprintln!("bridge signer exited with error:\n{e}");

@@ -0,0 +1,30 @@
# A list of filter directives of the form target[span{field=value}]=level.
ASTRIA_BRIDGE_SIGNER_LOG=astria_bridge_signer=info
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is indeed standard, but it's wrong.


build:
if: ${{ always() && !cancelled() }}
needs: [cli, composer, conductor, sequencer, relayer, bridge-withdrawer]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort alphabetically?

let sequencer_asset_str = contract
.base_chain_asset_denomination()
.await
.wrap_err("failed to get base chain asset denomination")?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get base chain asset from where?

.wrap_err("failed to get block")?
.ok_or_eyre("block not found")?;
let actions: Vec<Action> = getter
.get_for_block_hash(block.hash.ok_or_eyre("block hash is None")?)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

block hash was empty or is not set

let tx = provider
.get_transaction_receipt(tx_hash)
.await
.wrap_err("failed to get transaction")?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get the transaction from where?

Ok(actions)
}

fn parse_rollup_withdrawal_event_id(event_id: &str) -> eyre::Result<(H256, usize)> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear what this is supposed to be parsing. Is this returning a withdrawal event ID? This looks more like a struct TxHashWithEventIndex { tx_hash: H256, index: usize, }.

We should also type the errors (enum) and impl FromStr.

}

fn parse_rollup_withdrawal_event_id(event_id: &str) -> eyre::Result<(H256, usize)> {
let regex = regex::Regex::new(r"^(0x[0-9a-fA-F]+).(0x[0-9a-fA-F]+)$")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be put into a LazyCell or OnceCell.

@joroshiba
Copy link
Member

This PR is stale because it has been open 45 days with no activity. Remove stale label or this PR will be
closed in 7 days.

@joroshiba
Copy link
Member

This PR was closed because it has been stale.

@joroshiba joroshiba closed this Jul 6, 2025
sgranfield4403-3 added a commit to sgranfield4403-3/astria that referenced this pull request Oct 2, 2025
## Summary
support FROST ed25519 threshold signing in the bridge withdrawer. 

## Background
improves security of the withdrawer over having one private key used;
instead, we can employ an m-of-n threshold signature scheme to sign
withdrawals.

## Changes
- create `FrostParticipantService` gRPC proto definition, which is to be
implemented by a binary which contains a FROST secret key partial that
participates in the threshold signing process
- update withdrawer config/setup to support single signer (previous
behaviour) or threshold signing via the `Signer` trait
- create `FrostSigner` which has gRPC clients for the signing
participants
- `FrostSigner.sign()` performs the signing process by calling each
participant for their commitment (part 1) and signature share (part 2)
and finally aggregates the shares to create a valid signature.
- `FrostSigner.sign()` will fail if it does not receive responses from
at least the minimum number of signers required. the min number of
signers is determined during key generation, which is done completely
separately.

## Testing
blackbox tests with mock signer servers have been added.

also, this was tested with a 2/2 threshold signer node setup. withdrawal
transactions are successfully signed, submitted, and included on the
sequencer. see
https://www.notion.so/astria-org/bridge-threshold-withdrawer-testing-1936bd31a90c803ab33ccc1221f545a1?pvs=4

use the following testing keys which are for 2/2 threshold and represent
astria address `astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh`

put the following keys into their own files.

pubkey package:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "verifying_shares": {
    "0100000000000000000000000000000000000000000000000000000000000000": "bd209bd0c47806fee2db29bde0b705f6a191602f0307e8d959d74f189ae92e9e",
    "0200000000000000000000000000000000000000000000000000000000000000": "0a677e1c6ad0c1906bf6ac53810b29efcb9ca9e7ab50fe9fca3ae49e613b922f"
  },
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69"
```

secret key 1:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "identifier": "0100000000000000000000000000000000000000000000000000000000000000",
  "signing_share": "4b45de055567d7a2027bfc9a2463de4aaa6f2df995180e638dc938013e9bf70e",
  "verifying_share": "bd209bd0c47806fee2db29bde0b705f6a191602f0307e8d959d74f189ae92e9e",
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69",
  "min_signers": 2
}
```

secret key 2:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "identifier": "0200000000000000000000000000000000000000000000000000000000000000",
  "signing_share": "29a52cd248df9a7bb2346fb4ed53318f0bff320985c8e49062c68e4fd0012d0f",
  "verifying_share": "0a677e1c6ad0c1906bf6ac53810b29efcb9ca9e7ab50fe9fca3ae49e613b922f",
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69",
  "min_signers": 2
}
```

checkout `noot/frost-participant` (see
astriaorg/astria#1960)

build astria-cli, astria-bridge-withdrawer, astria-bridge-signer and
astria-sequencer.

run anvil (using this as fake rollup evm node for testing)

run astria-sequencer + cometbft as normal

transfer funds from funded address to withdrawer address

```bash
./target/debug/astria-cli sequencer transfer --amount 100000000 \
--private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 \
--sequencer-url http://localhost:26657 \
--sequencer.chain-id astria astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh
```

initialize bridge account on astria, with
`astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh` being the withdrawer
address

```bash
./target/debug/astria-cli sequencer init-bridge-account \
--private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 \
--withdrawer-address astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh \
--sequencer-url http://localhost:26657 \
--sequencer.chain-id astria \
--rollup-name test
```

result:

```bash
sending tx from address: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm
InitBridgeAccount completed!
Included in block: 1252
Rollup name: test
Rollup ID: n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg=
```

deploy contracts to anvil using `astria-bridge-contracts` 

- set `.env` and `source .env`

```bash
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
RPC_URL="http://localhost:8545"
BASE_CHAIN_ASSET_PRECISION=9
BASE_CHAIN_BRIDGE_ADDRESS="astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm"
BASE_CHAIN_ASSET_DENOMINATION="nria"
```

- `forge script script/AstriaWithdrawer.s.sol:AstriaWithdrawerScript
--rpc-url $RPC_URL --broadcast --sig "deploy()" -vvvv`
- contract deployed at `0x5FbDB2315678afecb367f032d93F642f64180aa3` ,
update `.env` again

run two astria-bridge-signers, one with grpc port `9001` and first
secret key package, second with grpc port `9002` and second secret key
package.

start the bridge withdrawer, configured for 2/2 threshold (make sure to
update pubkey package path):

```bash
ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED=true
ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS=2
ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="pub_bridge.key"
ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="http://127.0.0.1:9001,http://127.0.0.1:9002"
ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_BRIDGE_ADDRESS="astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm"
ASTRIA_BRIDGE_WITHDRAWER_ETHEREUM_CONTRACT_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"

```

finally, send a withdrawal on the rollup:

```bash
forge script script/AstriaWithdrawer.s.sol:AstriaWithdrawerScript \
   --rpc-url $RPC_URL --broadcast --sig "withdrawToSequencer()" -vvvv
```

in the withdrawer, should see:

```bash
2025-02-10T17:33:57.417370Z  INFO process_batch: astria_bridge_withdrawer::bridge_withdrawer::submitter: withdraw batch successfully executed. sequencer.block=41 sequencer.tx_hash=69E3997A2A67CD4716D1051D9962219FBCEF9F3F7557C276D9F4D414D30166F2 rollup.height=4 batch.value=1000
```

## Changelogs
Changelogs updated.

## Related Issues

closes  #1937

---------

Co-authored-by: Richard Janis Goldschmidt <github@aberrat.io>
AngieD101 added a commit to AngieD101/astria that referenced this pull request Oct 10, 2025
## Summary
support FROST ed25519 threshold signing in the bridge withdrawer. 

## Background
improves security of the withdrawer over having one private key used;
instead, we can employ an m-of-n threshold signature scheme to sign
withdrawals.

## Changes
- create `FrostParticipantService` gRPC proto definition, which is to be
implemented by a binary which contains a FROST secret key partial that
participates in the threshold signing process
- update withdrawer config/setup to support single signer (previous
behaviour) or threshold signing via the `Signer` trait
- create `FrostSigner` which has gRPC clients for the signing
participants
- `FrostSigner.sign()` performs the signing process by calling each
participant for their commitment (part 1) and signature share (part 2)
and finally aggregates the shares to create a valid signature.
- `FrostSigner.sign()` will fail if it does not receive responses from
at least the minimum number of signers required. the min number of
signers is determined during key generation, which is done completely
separately.

## Testing
blackbox tests with mock signer servers have been added.

also, this was tested with a 2/2 threshold signer node setup. withdrawal
transactions are successfully signed, submitted, and included on the
sequencer. see
https://www.notion.so/astria-org/bridge-threshold-withdrawer-testing-1936bd31a90c803ab33ccc1221f545a1?pvs=4

use the following testing keys which are for 2/2 threshold and represent
astria address `astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh`

put the following keys into their own files.

pubkey package:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "verifying_shares": {
    "0100000000000000000000000000000000000000000000000000000000000000": "bd209bd0c47806fee2db29bde0b705f6a191602f0307e8d959d74f189ae92e9e",
    "0200000000000000000000000000000000000000000000000000000000000000": "0a677e1c6ad0c1906bf6ac53810b29efcb9ca9e7ab50fe9fca3ae49e613b922f"
  },
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69"
```

secret key 1:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "identifier": "0100000000000000000000000000000000000000000000000000000000000000",
  "signing_share": "4b45de055567d7a2027bfc9a2463de4aaa6f2df995180e638dc938013e9bf70e",
  "verifying_share": "bd209bd0c47806fee2db29bde0b705f6a191602f0307e8d959d74f189ae92e9e",
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69",
  "min_signers": 2
}
```

secret key 2:

```bash
{
  "header": {
    "version": 0,
    "ciphersuite": "FROST-ED25519-SHA512-v1"
  },
  "identifier": "0200000000000000000000000000000000000000000000000000000000000000",
  "signing_share": "29a52cd248df9a7bb2346fb4ed53318f0bff320985c8e49062c68e4fd0012d0f",
  "verifying_share": "0a677e1c6ad0c1906bf6ac53810b29efcb9ca9e7ab50fe9fca3ae49e613b922f",
  "verifying_key": "ecbe5e2fbaaf23f14ca43f34d71ad733c57afcadeb5782809af48a3da17b9b69",
  "min_signers": 2
}
```

checkout `noot/frost-participant` (see
astriaorg/astria#1960)

build astria-cli, astria-bridge-withdrawer, astria-bridge-signer and
astria-sequencer.

run anvil (using this as fake rollup evm node for testing)

run astria-sequencer + cometbft as normal

transfer funds from funded address to withdrawer address

```bash
./target/debug/astria-cli sequencer transfer --amount 100000000 \
--private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 \
--sequencer-url http://localhost:26657 \
--sequencer.chain-id astria astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh
```

initialize bridge account on astria, with
`astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh` being the withdrawer
address

```bash
./target/debug/astria-cli sequencer init-bridge-account \
--private-key 2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90 \
--withdrawer-address astria1w2lxx9p02u7u934ljl060wannqm40g3y089lmh \
--sequencer-url http://localhost:26657 \
--sequencer.chain-id astria \
--rollup-name test
```

result:

```bash
sending tx from address: astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm
InitBridgeAccount completed!
Included in block: 1252
Rollup name: test
Rollup ID: n4bQgYhMfWWaL-qgxVrQFaO_TxsrC4Is0V1sFbDwCgg=
```

deploy contracts to anvil using `astria-bridge-contracts` 

- set `.env` and `source .env`

```bash
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
RPC_URL="http://localhost:8545"
BASE_CHAIN_ASSET_PRECISION=9
BASE_CHAIN_BRIDGE_ADDRESS="astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm"
BASE_CHAIN_ASSET_DENOMINATION="nria"
```

- `forge script script/AstriaWithdrawer.s.sol:AstriaWithdrawerScript
--rpc-url $RPC_URL --broadcast --sig "deploy()" -vvvv`
- contract deployed at `0x5FbDB2315678afecb367f032d93F642f64180aa3` ,
update `.env` again

run two astria-bridge-signers, one with grpc port `9001` and first
secret key package, second with grpc port `9002` and second secret key
package.

start the bridge withdrawer, configured for 2/2 threshold (make sure to
update pubkey package path):

```bash
ASTRIA_BRIDGE_WITHDRAWER_FROST_THRESHOLD_SIGNING_ENABLED=true
ASTRIA_BRIDGE_WITHDRAWER_FROST_MIN_SIGNERS=2
ASTRIA_BRIDGE_WITHDRAWER_FROST_PUBLIC_KEY_PACKAGE_PATH="pub_bridge.key"
ASTRIA_BRIDGE_WITHDRAWER_FROST_PARTICIPANT_ENDPOINTS="http://127.0.0.1:9001,http://127.0.0.1:9002"
ASTRIA_BRIDGE_WITHDRAWER_SEQUENCER_BRIDGE_ADDRESS="astria1rsxyjrcm255ds9euthjx6yc3vrjt9sxrm9cfgm"
ASTRIA_BRIDGE_WITHDRAWER_ETHEREUM_CONTRACT_ADDRESS="0x5FbDB2315678afecb367f032d93F642f64180aa3"

```

finally, send a withdrawal on the rollup:

```bash
forge script script/AstriaWithdrawer.s.sol:AstriaWithdrawerScript \
   --rpc-url $RPC_URL --broadcast --sig "withdrawToSequencer()" -vvvv
```

in the withdrawer, should see:

```bash
2025-02-10T17:33:57.417370Z  INFO process_batch: astria_bridge_withdrawer::bridge_withdrawer::submitter: withdraw batch successfully executed. sequencer.block=41 sequencer.tx_hash=69E3997A2A67CD4716D1051D9962219FBCEF9F3F7557C276D9F4D414D30166F2 rollup.height=4 batch.value=1000
```

## Changelogs
Changelogs updated.

## Related Issues

closes  #1937

---------

Co-authored-by: Richard Janis Goldschmidt <github@aberrat.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bridge-signer code touching the bridge-signer crate cd ci issues that are related to ci and github workflows closed-stale stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support threshold signing in bridge withdrawer

5 participants