> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Request and verify a threshold signature

> Request a signature from Metal's validators and verify a document approval

> [!NOTE]
>
> **What you'll be able to do**
>
> * Request a validator signature for a document approval.
> * Verify the signature against the approved message and network signing key.

> [!WARNING]
>
> The `FrostRequests` queue is [not on Metal Testnet yet](https://docs.metalntwx.com/reference/contract-addresses.md#contracts-not-yet-on-metal-testnet).
> Follow this guide on a network that has code at its address.

A **threshold signature** is a single signature created jointly by validators.
Each validator holds a share of the signing key, and a minimum number of them,
called the **threshold**, must participate. The result can be verified with one
public key for the group.

This distributes signing authority across validators. Applications can use these
signatures to send a document approval to another service or authorize a
transaction on a chain that supports the chosen signature format.

`FrostRequests` is Metal's contract for queuing these signing requests. An
application contract admitted by the chain authority validates the action and
submits its message to the queue. Validators sign it after the request's block
finalizes. This guide requests a signature for a document approval, retrieves
it, and verifies it against the group's public key.

## Obtain network and requester access

Obtain the network's signing configuration, Ed25519 group public key, and HTTPS
endpoint for signing results from the network operator. Use the signing result
service to retrieve signatures, since public observer nodes do not retain them.

Check for code at the queue, `0x000000000000000000000000000000000000f057`,
and arrange authority approval for your requester contract.

Verify the group key, its network, and its signing suite through an
authenticated operator channel, and use that channel to verify later key
changes. This guide uses suite `1`, named `ed25519`, with a 32-byte group key
and a 64-byte Ed25519 signature. Suite `0`, named `secp256k1-tr`, uses BIP-340
and requires a different verifier.

## Authorize and bind the message

The `DocumentApproval` requester records one document approval per application
action ID. Its owner approves a nonzero document hash once, and a repeated
signing request uses that same approval. Adapt the owner's authorization rule to
your application's requirements before the contract is admitted to the queue.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IFrostRequests {
    function request(uint8 suite, bytes calldata message) external returns (bytes32);
}

contract DocumentApproval {
    IFrostRequests constant QUEUE = IFrostRequests(0x000000000000000000000000000000000000F057);
    address public immutable owner = msg.sender;
    mapping(bytes32 => bytes32) public approvedDocument;
    event Authorized(bytes32 indexed actionId, bytes32 documentHash);

    function messageFor(bytes32 actionId) public view returns (bytes memory) {
        bytes32 documentHash = approvedDocument[actionId];
        require(documentHash != bytes32(0), "unknown action");
        return abi.encode("METAL_DOCUMENT_APPROVAL_V1", block.chainid, address(this),
            actionId, documentHash);
    }

    function authorize(bytes32 actionId, bytes32 documentHash) external returns (bytes32) {
        require(msg.sender == owner, "owner");
        require(actionId != bytes32(0) && documentHash != bytes32(0), "empty approval");
        require(approvedDocument[actionId] == bytes32(0), "duplicate action");
        approvedDocument[actionId] = documentHash;
        emit Authorized(actionId, documentHash);
        return QUEUE.request(1, messageFor(actionId));
    }

    function requestAgain(bytes32 actionId) external returns (bytes32) {
        require(msg.sender == owner, "owner");
        return QUEUE.request(1, messageFor(actionId));
    }
}
```

[Deploy the contract](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md), then have
the authority admit its address. Read `allowedRequesters(application)` on the
queue and require `true` before submitting an approval transaction. The queue
rejects messages outside 1–4096 bytes and unknown suite IDs. If queue admission
fails, the whole `authorize` transaction reverts, including its approval state.

The signed message binds an application version, chain ID, requester address,
action ID, and document hash. A consumer checks those fields and records the
action ID when it applies the approval, preventing the same approval from being
applied twice.

## Submit and record the request

Assign a unique `actionId` and compute `documentHash` from the exact document
bytes to approve. Use the
[SDK signing procedure](https://docs.metalntwx.com/reference/typescript-sdk.md#signmip20contract) to prepare
`authorize(actionId, documentHash)`, with the
[estimated fee](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md). Persist its signed
transaction hash and bytes before broadcast.

After a successful receipt, follow
[canonical finality verification](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md) and
[settlement semantics](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md). Decode `SignatureRequested` only
from the queue's address and `Authorized` only from the admitted requester.
Compare the exact message bytes with the locally encoded message:

```typescript theme={null}
import { encodeAbiParameters, encodePacked, keccak256, parseAbi, parseAbiParameters } from 'viem'

const queue = '0x000000000000000000000000000000000000f057' as const
const queueAbi = parseAbi([
  'function allowedRequesters(address requester) view returns (bool)',
  'function request(uint8 suite, bytes message) returns (bytes32)',
  'event SignatureRequested(bytes32 indexed id, uint8 suite, bytes message)',
])
const message = encodeAbiParameters(
  parseAbiParameters('string, uint256, address, bytes32, bytes32'),
  ['METAL_DOCUMENT_APPROVAL_V1', 63825n, application, actionId, documentHash],
)
const requestId = keccak256(encodePacked(['uint8', 'bytes'], [1, message]))
```

`application` is the admitted requester address. Require the queue event's suite
to equal `1`, its message to equal `message`, and its ID to equal `requestId`.
Read `approvedDocument(actionId)` and `messageFor(actionId)` at the receipt
block and compare them with the saved inputs. Once the request is finalized,
poll for its signature.

## Poll the result service

Send this JSON-RPC request to the operator-provided endpoint for signing
results, replacing the ID with the saved 32-byte request ID:

```json theme={null}
{"jsonrpc":"2.0","id":1,"method":"frost_signature","params":["0x<request-id>"]}
```

A completed response contains `suite`, `signature`, and `groupKey` in `result`,
with bytes hex-encoded. Match the response to the RPC request ID and verify its
signature against the message saved for that request.

`result: null` means the service has no stored result, either because signing is
pending or because the result is unavailable. Set a polling deadline and leave
the application approval pending if it expires. Keep the same request ID through
RPC errors and restarts.

The method `frost_groupKey` accepts `["ed25519"]` and returns the service's key
or `null`. Compare that key with the authenticated operator configuration to
check that the service uses the expected key.

## Verify the exact bytes and trusted key

Use Node.js's Ed25519 verifier with `trustedGroupKey` from the authenticated
operator configuration. It checks the result's suite and key, then verifies the
signature over the saved message bytes.

```typescript theme={null}
import { createPublicKey, verify } from 'node:crypto'
import { hexToBytes, size, type Hex } from 'viem'

type SigningResult = { suite: string, signature: Hex, groupKey: Hex }

function verifyApproval(result: SigningResult, message: Hex, trustedGroupKey: Hex): boolean {
  try {
    if (result.suite !== 'ed25519') return false
    if (size(trustedGroupKey) !== 32 || size(result.signature) !== 64) return false
    if (result.groupKey.toLowerCase() !== trustedGroupKey.toLowerCase()) return false
    const key = createPublicKey({
      key: Buffer.concat([
        Buffer.from('302a300506032b6570032100', 'hex'),
        Buffer.from(hexToBytes(trustedGroupKey)),
      ]),
      format: 'der', type: 'spki',
    })
    return verify(null, hexToBytes(message), key, hexToBytes(result.signature))
  } catch {
    return false
  }
}
```

Pass the raw ABI-encoded message bytes to verification. Record the authenticated
key identifier, suite, request ID, message, signature, and finalized request
transaction. Apply the approval only after all checks pass.

## Check duplicates and recovery

Call `requestAgain(actionId)` to test a duplicate request. It emits another
queue event for the same suite and message, so the request ID stays the same and
validators can return the existing signature. Verify that `approvedDocument`
stays unchanged and only the original `Authorized` event exists. A different
suite or message produces a different ID and requires separate application
authorization.

If submission times out, query the saved transaction hash to recover its
receipt. Resume signature polling with the saved request ID after an
interruption. Test a wrong key, changed message, malformed signature, pending
result, and duplicate action, then verify a network signature against the saved
message and authenticated group key.

For a payment on another chain, the signed message must follow that chain's
transaction format and authorization rules. Apply its replay, broadcast, and
finality rules when submitting the transaction.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.