Guide de l'utilisateur sur le calcul multipartite

Ce guide explique comment utiliser l'API Multi-party Compute pour gérer les clés distribuées, les participants et les règles. Il est destiné aux clients de la version preview qui découvrent les principaux parcours utilisateur.

Présentation

L'API MPC se compose de deux éléments principaux :

  1. Core MPC gère le cycle de vie des clés distribuées, des propositions et des cérémonies de signature.
  2. Le moteur de stratégies applique la gouvernance en autorisant les opérations avant leur exécution.

Pour obtenir une description détaillée de l'interaction entre l'accès à l'API et la signature cryptographique, consultez Sécurité et autorisation.

Prérequis

Avant de commencer, assurez-vous de disposer des éléments suivants :

  1. A Google Cloud Project. Consultez Créer des projets pour en créer un.
  2. Si vous n'utilisez pas Cloud Shell, la CLI gcloud doit être installée et authentifiée.
  3. Les API requises doivent être activées, si ce n'est pas déjà fait. Vous en avez besoin de deux : la nouvelle API MPC utilisée dans ce guide et Cloud Key Management Service si vous l'utilisez pour générer/signer la clé d'approbation.

      gcloud services enable mpc.googleapis.com cloudkms.googleapis.com
    

    Pour afficher les API activées :

      gcloud services list --enabled
    
  4. Python 3.9 ou version ultérieure et les bibliothèques d'aide du tutoriel sont installés :

      pip install embit eth-account eth-utils requests
    
  5. (Facultatif) Si votre organisation s'authentifie à l'aide d'un fournisseur d'identité externe (comme Okta ou Microsoft Entra ID) sans compte Google, consultez Configurer la fédération des identités des employés (BYOID) pour configurer les Identifiants par défaut de l'application (ADC) avec votre identité fédérée.

Configuration

Mettez à jour les variables suivantes pour renseigner les commandes de ce guide.

Configuration requise

Vous devez personnaliser ces paramètres. Le guide ne fonctionnera pas avec les valeurs d'espace réservé.

Paramètre Valeur Description
ID du projet my-project L'ID du projet client que vous nous avez fourni pour l'intégration à la version preview.
ID de domaine 67317436-0f29-ba9e-9146-d11c0bd8df9b ID de domaine unique que nous avons attribué à votre instance.
ID du chemin d'accès de l'instance mpc-unit-bob-123 ID d'instance de chemin d'accès que nous avons attribué à votre instance.
Emplacement us-central1 Emplacement du déploiement. Doit être us-central1 (les autres régions ne sont pas compatibles avec la version preview).

Libellés de ressource du tutoriel

Il s'agit des noms des ressources créées au cours de ce tutoriel. Les valeurs par défaut sont valides et peuvent être conservées pour minimiser les difficultés de configuration.

Paramètre Valeur Description
ID du trousseau de clés Cloud KMS mpc-assent-keys Trousseau de clés Cloud Key Management Service pour les clés d'approbation externes stockées dans Cloud KMS.
ID du signataire 1 signer-alice Signataire MPC 1.
ID du signataire 2 signer-bob Signataire MPC 2.
ID du signataire 3 signer-charlie Signataire MPC 3.
ID de l'auteur de la règle 1 author-dave Auteur de la règle 1.
ID de l'auteur de la règle 2 author-eve Auteur de la règle 2.
ID de la règle ethereum-transfer-policy ID de la règle (par exemple, ethereum-transfer-policy ou bitcoin-psbt-policy).
ID du trousseau de clés my-key-ring ID du trousseau de clés.
ID de clé my-key ID de clé.
ID de la proposition my-proposal ID de la proposition.

Étape 1 : Générer des clés d'approbation

Pour utiliser l'API MPC, vous devez générer des paires de clés pour les rôles distincts suivants. Notez qu'il s'agit de clés d'approbation cryptographiques externes, qui sont distinctes des fragments de clé de seuil MPC distribués générés en interne lors de la configuration.

  • Auteurs de règles : autorisez les règles de gouvernance.
  • Signataires : donnent leur accord (vote/approbation) sur les propositions.

A. Clés des participants (secp256k1)

Générez les clés cryptographiques nécessaires pour vos participants. Ces commandes sont idempotentes.

Cloud KMS

Nous vous recommandons de gérer les clés d'approbation dans Cloud Key Management Service. Assurez-vous que votre identité dispose des autorisations nécessaires pour créer des trousseaux de clés, créer des clés, signer et afficher les vérificateurs de clés publiques (comme roles/cloudkms.admin et roles/cloudkms.signerVerifier). Assurez-vous d'avoir créé un KeyRing, puis générez une clé secp256k1 pour tous vos signataires.

provision_keyring() {
  if gcloud kms keyrings describe "mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KeyRing 'mpc-assent-keys' already exists."
  else
    echo "KeyRing 'mpc-assent-keys' not found. Creating it..."
    gcloud kms keyrings create "mpc-assent-keys" --location="us-central1"
  fi
}

provision_kms_key() {
  local id=$1
  if gcloud kms keys describe "${id}-key" --keyring="mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KMS key '${id}-key' already exists."
  else
    echo "KMS key '${id}-key' not found. Creating it..."
    gcloud kms keys create "${id}-key" \
      --keyring="mpc-assent-keys" \
      --location="us-central1" \
      --purpose=asymmetric-signing \
      --default-algorithm=ec-sign-secp256k1-sha256 \
      --protection-level=hsm
  fi

  gcloud kms keys versions get-public-key 1 \
    --key="${id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --output-file="${id}_pub.pem"

  echo "Retrieved public key from KMS and saved to ${id}_pub.pem:"
  cat "${id}_pub.pem"
}

# Run Setup
provision_keyring

# Generate Keys
for ID in signer-alice signer-bob signer-charlie; do
  echo "Generating KMS key for $ID..."
  provision_kms_key "$ID"
done

OpenSSL

Vous pouvez utiliser OpenSSL standard pour générer des clés secp256k1 pour chaque participant.

provision_openssl_key() {
  local id=$1
  if [[ ! -f "${id}_priv.pem" ]]; then
    openssl ecparam -name secp256k1 -genkey -noout -out "${id}_priv.pem"
    openssl ec -in "${id}_priv.pem" -pubout -out "${id}_pub.pem"
    echo "Wrote private key to ${id}_priv.pem and public key to ${id}_pub.pem:"
    cat "${id}_pub.pem"
  else
    echo "Private key file '${id}_priv.pem' already exists. Skipping OpenSSL generation."
    echo "Using existing public key from '${id}_pub.pem':"
    cat "${id}_pub.pem"
  fi
}

for ID in signer-alice signer-bob signer-charlie; do
  echo "Generating OpenSSL key for $ID..."
  provision_openssl_key "$ID"
done

B. Clés de l'auteur de la règle (secp256k1)

Cloud KMS

De même, nous vous recommandons de gérer ces clés d'autorisation dans Cloud Key Management Service. Assurez-vous que votre identité dispose des autorisations nécessaires pour créer des trousseaux de clés, créer des clés, signer et afficher les vérificateurs de clés publiques (comme roles/cloudkms.admin et roles/cloudkms.signerVerifier). Assurez-vous d'avoir créé un KeyRing, puis générez une clé secp256k1 pour tous vos auteurs de règles.

provision_keyring() {
  if gcloud kms keyrings describe "mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KeyRing 'mpc-assent-keys' already exists."
  else
    echo "KeyRing 'mpc-assent-keys' not found. Creating it..."
    gcloud kms keyrings create "mpc-assent-keys" --location="us-central1"
  fi
}

provision_kms_key() {
  local id=$1
  if gcloud kms keys describe "${id}-key" --keyring="mpc-assent-keys" --location="us-central1" > /dev/null 2>&1; then
    echo "KMS key '${id}-key' already exists."
  else
    echo "KMS key '${id}-key' not found. Creating it..."
    gcloud kms keys create "${id}-key" \
      --keyring="mpc-assent-keys" \
      --location="us-central1" \
      --purpose=asymmetric-signing \
      --default-algorithm=ec-sign-secp256k1-sha256 \
      --protection-level=hsm
  fi

  gcloud kms keys versions get-public-key 1 \
    --key="${id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --output-file="${id}_pub.pem"

  echo "Retrieved public key from KMS and saved to ${id}_pub.pem:"
  cat "${id}_pub.pem"
}

provision_keyring

for ID in author-dave author-eve; do
  echo "Generating KMS key for $ID..."
  provision_kms_key "$ID"
done

OpenSSL

Les auteurs de règles utilisent la courbe secp256k1 pour signer les règles de gouvernance et les actions administratives. Vous pouvez générer ces clés à l'aide d'OpenSSL standard.

provision_openssl_key() {
  local id=$1
  if [[ ! -f "${id}_priv.pem" ]]; then
    openssl ecparam -name secp256k1 -genkey -noout -out "${id}_priv.pem"
    openssl ec -in "${id}_priv.pem" -pubout -out "${id}_pub.pem"
    echo "Wrote private key to ${id}_priv.pem and public key to ${id}_pub.pem:"
    cat "${id}_pub.pem"
  else
    echo "Private key file '${id}_priv.pem' already exists. Skipping OpenSSL generation."
    echo "Using existing public key from '${id}_pub.pem':"
    cat "${id}_pub.pem"
  fi
}

for ID in author-dave author-eve; do
  echo "Generating OpenSSL key for $ID..."
  provision_openssl_key "$ID"
done

Étape 2 : Enregistrez les participants

Avant de pouvoir participer au flux MPC (en tant que signataire) ou au flux de règles (en tant qu'auteur de règles), une entité doit être enregistrée en tant que participant.

L'enregistrement nécessite une coordination hors bande entre l'administrateur et le participant :

  1. L'entité (signataire ou auteur) génère sa paire de clés localement et transfère sa clé publique à l'administrateur de manière sécurisée.
  2. L'administrateur (disposant des autorisations IAM Google Cloud standards) appelle l'API pour enregistrer la clé publique.

curl

provision_participant() {
  local id=$1
  local name=$2
  local pubkey_b64

  # Read the public key from the file generated in Step 1
  echo "Reading public key from ${id}_pub.pem..."
  # Public key must be represented as Base64 encoded bytes of the PEM/DER format.
  pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")

  # Check existence
  if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/$id"; then
    echo "Participant '$id' already exists. Skipping registration."
  else
    echo "Participant '$id' not found. Registering..."
    curl -X POST \
      -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
      -H "X-Goog-User-Project: my-project" \
      -H "Content-Type: application/json" \
      -d "{
        \"displayName\": \"$name\",
        \"activePublicAssentKey\": {
          \"publicKey\": \"$pubkey_b64\",
          \"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
        }
      }" \
      "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers?participant_id=$id" | jq
  fi
}

echo "Registering Signers..."
for ID in signer-alice signer-bob signer-charlie; do
  provision_participant "$ID" "Signer $ID"
done

echo "Registering Policy Authors..."
for ID in author-dave author-eve; do
  provision_participant "$ID" "Author $ID"
done

Étape 3 : Configurer le moteur de stratégies

Configurez le moteur de stratégies en configurant le bundle de confiance et en créant des règles.

A. Initialiser le bundle de confiance de l'auteur de la stratégie

Définissez qui sont les auteurs de règles autorisés. Il s'agit d'une ressource singleton pour l'instance.

curl

# Get current sequence from the API. If null (initial state), fallback to 0.
NEXT_BUNDLE_SEQUENCE=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle" \
  | jq -r '.sequenceNumber // 0')

# Calculate higher sequence number.
NEXT_BUNDLE_SEQUENCE=$((NEXT_BUNDLE_SEQUENCE + 1))

curl -X PATCH \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "sequenceNumber": '"$NEXT_BUNDLE_SEQUENCE"',
    "authors": [
      {"authorId": "'"author-dave"'"},
      {"authorId": "'"author-eve"'"}
    ]
  }' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle | jq

B. Créer une règle (PENDING_ACTIVE)

Définissez un ID de stratégie (ethereum-transfer-policy ou bitcoin-psbt-policy) et créez une règle décrivant les actions autorisées. Bien que vous puissiez configurer de nombreuses règles pour régir différents scénarios, ce guide explique comment configurer une règle pour les transactions Ethereum ou Bitcoin. Par défaut, le moteur de stratégies est défini sur Fail-fast (bloquer par défaut). Si aucune stratégie n'autorise explicitement une transaction, celle-ci est refusée.

Avant de créer ou d'approuver une règle, les auteurs doivent signer une charge utile représentant l'intention. Vous pouvez utiliser les fonctions d'assistance suivantes pour gérer la sérialisation et la signature binaires.

Ethereum

Cette règle autorise les transferts Ethereum de plus petite taille (<= 2 ETH, représentés par 2 000 000 000 Gwei) vers une adresse spécifique pour Alice.

1. Générer une signature de quorum

Utiliser Cloud KMS :

sign_policy() {
  local author_id=$1
  local sequence=$2
  local cedar_doc=$3
  local policy_id=$4
  export _HELPER_POLICY_ID="$policy_id"
  export _HELPER_SEQUENCE="$sequence"
  export _HELPER_CEDAR_DOC="$cedar_doc"
  python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")

Avec OpenSSL :

sign_policy() {
  local author_id=$1
  local sequence=$2
  local cedar_doc=$3
  local policy_id=$4
  export _HELPER_POLICY_ID="$policy_id"
  export _HELPER_SEQUENCE="$sequence"
  export _HELPER_CEDAR_DOC="$cedar_doc"
  python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin payload.bin
  openssl base64 -A -in policy_sig.bin
}

# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")

2. Appeler l'API CreatePolicy

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy"; then
  echo "Policy 'ethereum-transfer-policy' already exists. Skipping creation."
else
  echo "Policy 'ethereum-transfer-policy' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "displayName": "Ethereum Transfer Permit Policy",
      "rule": {
        "cedarDocument": "permit(principal == User::\"alice\", action == Action::\"Transfer\", resource == Resource::\"mpc_operation\") when { context.transaction.to_address == \"0x1111111111111111111111111111111111111111\" && context.transaction.value_gwei <= 2000000000 };"
      },
      "createAuthorSignature": {
        "authorId": "'"author-dave"'",
        "signature": "'"$QUORUM_SIGNATURE"'",
        "approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
      }
    }' \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies?policy_id=ethereum-transfer-policy" | jq
fi

Bitcoin

Cette règle autorise les transferts de bitcoins à l'aide du schéma bitcoin.transaction.v2 (BIP 174 PSBT) lorsque le montant net du transfert sortant ne dépasse pas 250 000 satoshis (context.transaction.spend_amount_scaled <= 2500000, ou 0,0025 BTC normalisé à 10-9 unités décimales fixes) et que les frais de minage ne dépassent pas 10 000 satoshis (context.transaction.max_fee_atomic <= 100000).

permit(
  principal == User::"alice",
  action == Action::"Transfer",
  resource == Resource::"mpc_operation"
)
when {
  context.transaction.schema == "bitcoin.transaction.v2" &&
  context.transaction.spend_amount_scaled <= 2500000 &&
  context.transaction.max_fee_atomic <= 100000
};

1. Générer une signature de quorum

Utiliser Cloud KMS :

sign_policy() {
  local author_id=$1
  local sequence=$2
  local cedar_doc=$3
  local policy_id=$4
  export _HELPER_POLICY_ID="$policy_id"
  export _HELPER_SEQUENCE="$sequence"
  export _HELPER_CEDAR_DOC="$cedar_doc"
  python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")

Avec OpenSSL :

sign_policy() {
  local author_id=$1
  local sequence=$2
  local cedar_doc=$3
  local policy_id=$4
  export _HELPER_POLICY_ID="$policy_id"
  export _HELPER_SEQUENCE="$sequence"
  export _HELPER_CEDAR_DOC="$cedar_doc"
  python3 -c "import os; p='my-project'; l='us-central1'; i='mpc-unit-bob-123'; pol=os.environ.get('_HELPER_POLICY_ID'); s=int(os.environ.get('_HELPER_SEQUENCE')); d=os.environ.get('_HELPER_CEDAR_DOC'); n=f'projects/{p}/locations/{l}/instances/{i}/policyEngine/policies/{pol}'; payload=b'action:ACTIVE\n'+n.encode('utf-8')+b'sequence:'+s.to_bytes(8,'little')+b'cedar_document:'+d.encode('utf-8'); open('payload.bin', 'wb').write(payload)"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin payload.bin
  openssl base64 -A -in policy_sig.bin
}

# Sign as Author 1 (Sequence 0)
QUORUM_SIGNATURE=$(sign_policy "author-dave" 0 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")

2. Appeler l'API CreatePolicy

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy"; then
  echo "Policy 'bitcoin-psbt-policy' already exists. Skipping creation."
else
  echo "Policy 'bitcoin-psbt-policy' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "displayName": "Bitcoin PSBT Permit Policy",
      "rule": {
        "cedarDocument": "permit(principal == User::\"alice\", action == Action::\"Transfer\", resource == Resource::\"mpc_operation\") when { context.transaction.schema == \"bitcoin.transaction.v2\" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };"
      },
      "createAuthorSignature": {
        "authorId": "'"author-dave"'",
        "signature": "'"$QUORUM_SIGNATURE"'",
        "approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
      }
    }' \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies?policy_id=bitcoin-psbt-policy" | jq
fi

C. Approuver la règle (passer à l'état ACTIVE)

Si la configuration de votre stratégie nécessite des signatures M-of-N, les auteurs suivants appellent AddQuorumSignature.

Pour générer la signature, vous devez d'abord récupérer la séquence actuelle de la stratégie.

Ethereum

# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy")

POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
  echo "Policy Sequence: $POLICY_SEQUENCE"

  # 2. Sign as Author 2
  QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.to_address == "0x1111111111111111111111111111111111111111" && context.transaction.value_gwei <= 2000000000 };' "ethereum-transfer-policy")

  # 3. Call AddQuorumSignature
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy",
      "signature": {
        "authorId": "'"author-eve"'",
        "signature": "'"$QUORUM_SIGNATURE_SEQ"'",
        "approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
      }
    }' \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy 'ethereum-transfer-policy' is already ACTIVE. Skipping signature."
else
  echo "Policy 'ethereum-transfer-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi

Bitcoin

# 1. Get the policy state and sequence
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy")

POLICY_STATE=$(echo "$POLICY_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  POLICY_SEQUENCE=$(echo "$POLICY_INFO" | jq -r '.globalSequence // 0')
  echo "Policy Sequence: $POLICY_SEQUENCE"

  # 2. Sign as Author 2
  QUORUM_SIGNATURE_SEQ=$(sign_policy "author-eve" $POLICY_SEQUENCE 'permit(principal == User::"alice", action == Action::"Transfer", resource == Resource::"mpc_operation") when { context.transaction.schema == "bitcoin.transaction.v2" && context.transaction.spend_amount_scaled <= 2500000 && context.transaction.max_fee_atomic <= 100000 };' "bitcoin-psbt-policy")

  # 3. Call AddQuorumSignature
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy",
      "signature": {
        "authorId": "'"author-eve"'",
        "signature": "'"$QUORUM_SIGNATURE_SEQ"'",
        "approvalTime": "'"$(date -u +"%Y-%m-%dT%H:%M:%SZ")"'"
      }
    }' \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/bitcoin-psbt-policy:addQuorumSignature" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy 'bitcoin-psbt-policy' is already ACTIVE. Skipping signature."
else
  echo "Policy 'bitcoin-psbt-policy' is in an unknown or unexpected state: $POLICY_STATE"
fi

Étape 4 : Créez une clé

Nous allons maintenant créer un trousseau de clés et générer une clé distribuée.

A. Créer un trousseau

Regroupement logique de vos clés MPC.

curl

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring"; then
  echo "KeyRing 'my-key-ring' already exists. Skipping creation."
else
  echo "KeyRing 'my-key-ring' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{}' \
  https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings?key_ring_id=my-key-ring | jq
fi

B. Générer une clé distribuée (DKG)

Lancez une cérémonie asynchrone pour générer une clé distribuée entre les participants.

curl

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key"; then
  echo "Key 'my-key' already exists. Skipping generation."
else
  echo "Key 'my-key' not found. Generating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "threshold": 2,
      "algorithm": "ECDSA_SECP256K1",
      "participantShares": [
        {"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice"},
        {"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-bob"},
        {"participantId": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-charlie"}
      ]
    }' \
  https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys?key_id=my-key | jq
fi

Cette opération renvoie une opération de longue durée (LRO). Vous devez attendre que la clé state passe à l'état ACTIVE (ce qui signifie que la cérémonie DKG a réussi).

🕵️‍♂️ Vérifier l'état de la clé (interrogation)

Interrogez régulièrement l'état de la clé en exécutant la commande suivante jusqu'à ce que le champ state de la réponse devienne ACTIVE (indiquant que la cérémonie DKG a réussi).

curl

curl -X GET \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key | jq

C. Dériver l'adresse blockchain et localiser les UTXO

Une fois que votre clé distribuée est ACTIVE, la clé MPC est prête à être utilisée. Au cours de cette étape, vous allez dériver l'adresse spécifique à la blockchain à partir de la clé publique non compressée de 65 octets renvoyée par la génération de clé MPC (0x04 || X || Y), et localiser les fonds non dépensés nécessaires aux transactions.

Ethereum

Les adresses Ethereum sont dérivées du hachage Keccak-256 du point de clé publique non compressé de 64 octets (pub_bytes[1:]), en prenant les 20 derniers octets.

Script de dérivation d'adresse (derive_address.py)

Exécutez le bloc suivant pour créer et exécuter derive_address.py :

cat <<'EOF' > derive_address.py
import base64
from eth_utils import keccak, to_checksum_address

# Insert your base64 public_key from Step 4.B
PUBLIC_KEY_B64 = "your-public-key"

pub_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(pub_bytes) == 65 and pub_bytes[0] == 4:
    raw_addr = "0x" + keccak(pub_bytes[1:])[-20:].hex()
    checksummed_addr = to_checksum_address(raw_addr)
    print("Ethereum Address:", checksummed_addr)
else:
    print(f"Error: Expected 65-byte uncompressed key, got {len(pub_bytes)} bytes.")
EOF
python3 derive_address.py

Financer l'adresse Ethereum

Avant de diffuser à l'étape 6, assurez-vous que votre adresse Ethereum dérivée dispose de suffisamment d'ETH sur le réseau de test Sepolia pour couvrir la valeur de la transaction et les frais de gaz. Vous pouvez demander des ETH de testnet sans frais en saisissant votre nouvelle adresse dérivée dans le Faucet pour Ethereum Sepolia.

Bitcoin

Les adresses Bitcoin sont dérivées de la clé publique EC secp256k1 non compressée de 65 octets (0x04 || X || Y). Plusieurs formats d'adresse sont compatibles avec la même clé publique :

  1. SegWit natif (P2WPKH / BIP 173) : adresse Bech32 (bc1q... sur Mainnet, tb1q... sur Testnet4) avec un témoin de 22 octets scriptPubKey (0014<hash160>). Il s'agit du format moderne recommandé, avec les frais de transaction les plus bas.
  2. SegWit imbriqué (P2SH-P2WPKH / BIP 49) : adresse SegWit encapsulée (3... sur Mainnet, 2... sur Testnet4) avec un scriptPubKey de 23 octets (a914<hash160>87) et un redeemScript de 22 octets (0014<hash160>), compatible avec les anciens systèmes.
  3. Ancienne (P2PKH/BIP 44) : adresse Base58 Pay-to-PubKey-Hash d'origine (1... sur Mainnet, m/n... sur Testnet4) avec un scriptPubKey de 25 octets (76a914<hash160>88ac).

Script de dérivation d'adresse (derive_btc_address.py)

Exécutez le bloc suivant pour créer et exécuter derive_btc_address.py :

cat <<'EOF' > derive_btc_address.py
"""Derives Bitcoin addresses (Native SegWit, Nested SegWit, Legacy) from an MPC public key using embit."""

import base64
from embit import ec, script
from embit.networks import NETWORKS

# Insert the Base64-encoded public key from Step 4.B (publicKey field, decodes to 65 raw bytes)
PUBLIC_KEY_B64 = "your-public-key"

raw_bytes = base64.b64decode(PUBLIC_KEY_B64)
if len(raw_bytes) != 65 or raw_bytes[0] != 4:
  raise ValueError(f"Expected 65-byte uncompressed public key (0x04...), got {len(raw_bytes)} bytes")

# 1. Parse and compress public key to 33 bytes
pub = ec.PublicKey.parse(raw_bytes)
pub.compressed = True
compressed_pubkey_hex = pub.sec().hex()

print("================================================================")
print(f"Compressed Public Key (33 bytes): {compressed_pubkey_hex}")
print("================================================================")

# 2. Native SegWit (P2WPKH - BIP 173)
p2wpkh = script.p2wpkh(pub)
print("\n[1. Native SegWit (P2WPKH / Bech32 - BIP 173)]")
print(f"  ScriptPubKey:           {p2wpkh.data.hex()}")
print(f"  Testnet4 Address (tb):  {p2wpkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (bc):   {p2wpkh.address(NETWORKS['main'])}")

# 3. Nested SegWit (P2SH-P2WPKH - BIP 49)
p2sh_p2wpkh = script.p2sh(p2wpkh)
print("\n[2. Nested SegWit (P2SH-P2WPKH / Base58 - BIP 49)]")
print(f"  RedeemScript:           {p2wpkh.data.hex()}")
print(f"  ScriptPubKey:           {p2sh_p2wpkh.data.hex()}")
print(f"  Testnet4 Address (2):   {p2sh_p2wpkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (3):    {p2sh_p2wpkh.address(NETWORKS['main'])}")

# 4. Legacy (P2PKH - BIP 44)
p2pkh = script.p2pkh(pub)
print("\n[3. Legacy (P2PKH / Base58 - BIP 44)]")
print(f"  ScriptPubKey:           {p2pkh.data.hex()}")
print(f"  Testnet4 Address (m/n): {p2pkh.address(NETWORKS['test'])}")
print(f"  Mainnet Address (1):    {p2pkh.address(NETWORKS['main'])}")
EOF

python3 derive_btc_address.py

Localiser les sorties non dépensées (UTXO)

Contrairement aux blockchains basées sur des comptes, les transactions Bitcoin dépensent des sorties de transaction non dépensées (UTXO) spécifiques. Avant de créer une transaction à l'étape 5 :

  1. Approvisionnez votre adresse Native SegWit dérivée (par exemple, à l'aide d'un faucet testnet).
  2. Interrogez les UTXO non dépensés de votre adresse à l'aide de votre nœud Bitcoin (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") ou d'une API d'explorateur de blocs.
  3. Notez les quatre paramètres suivants pour l'étape 5 :
    • txid : hachage de transaction de 32 octets de la sortie non dépensée.
    • vout : index de sortie basé sur zéro (par exemple, 0 ou 1).
    • value : montant non dépensé en satoshis (par exemple, 200000 satoshis = 0,002 BTC).
    • scriptPubKey : script de verrouillage hexadécimal (par exemple, 0014...).

Étape 5 : Exécuter une opération MPC

Exécutez une opération MPC en évaluant une transaction et en créant une proposition.

A. Évaluer la transaction (obtenir un jeton d'autorisation)

Avant de signer une opération MPC, autorisez la charge utile par rapport à vos règles pour recevoir un jeton JWS et le condensé de signature de 32 octets.

Ethereum

🛑 Cas A : Refus (valeur trop élevée)

Tentez d'autoriser une transaction de 3 ETH (ce qui dépasse notre limite de 2 ETH).

# Payload for 3 ETH (Hex equivalent contains value_gwei > 2B)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"

curl -X POST \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "principal": "User::\"alice\"",
    "action": "Action::\"Transfer\"",
    "resource": "Resource::\"mpc_operation\"",
    "schema": "ETHEREUM_TX_V1",
    "payload": "'"$RAW_TX_BYTES_HEX"'"
  }' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload | jq

Réponse attendue : decision: DENY

✅ Cas B : Approbation (valeur autorisée)

Tentez d'autoriser une transaction de 1 Gwei (dans la limite de 2 ETH).

# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "principal": "User::\"alice\"",
    "action": "Action::\"Transfer\"",
    "resource": "Resource::\"mpc_operation\"",
    "schema": "ETHEREUM_TX_V1",
    "payload": "'"$RAW_TX_BYTES_HEX"'"
  }' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload)

echo "$RESPONSE" | jq

# Automatically capture variables for next steps
DIGEST_HEX=$(echo "$RESPONSE" | jq -r .digest)
POLICY_ENGINE_TOKEN=$(echo "$RESPONSE" | jq -r .token)

# Convert to Base64 (which curl expects for bytes fields)
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
POLICY_ENGINE_TOKEN_B64=$(python3 -c "import base64; print(base64.b64encode('$POLICY_ENGINE_TOKEN'.encode('utf-8')).decode('utf-8'))")

Réponse attendue : decision: PERMIT avec un token.

Bitcoin

Dans le modèle UTXO de Bitcoin, les transactions dépensent des sorties non dépensées spécifiques. Dans SegWit (BIP 143), le calcul du hachage de signature nécessite de connaître le montant en satoshis et le script de verrouillage de l'entrée dépensée.

Le moteur de stratégies nécessite le format BIP 174 PSBT v0 (BITCOIN_TX_V2) avec PSBT_IN_WITNESS_UTXO renseigné pour chaque entrée.

Script de construction PSBT (build_psbt.py)

Exécutez le bloc suivant pour assembler une charge utile PSBT v0 BIP 174 valide :

cat <<'EOF' > build_psbt.py
"""Constructs a BIP 174 PSBT v0 payload using embit for Bitcoin policy evaluation."""

from embit import script, transaction
from embit.psbt import PSBT

# 1. Inputs (From Step 4.C)
PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")
INPUT_VALUE_SATS = int("200000")  # Total UTXO input value in satoshis
SENDER_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"

# 2. Output Allocations
RECIPIENT_SATS = int("50000")  # Recipient payment in satoshis
MINER_FEE_SATS = int("250")  # Miner fee in satoshis
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"

# Change Output calculation: InputSats - RecipientSats - MinerFeeSats
CHANGE_SATS = INPUT_VALUE_SATS - RECIPIENT_SATS - MINER_FEE_SATS
if CHANGE_SATS <= 0:
  raise ValueError(f"Insufficient input satoshis ({INPUT_VALUE_SATS}) for payment and fee")

# 3. Construct Unsigned Transaction (Version 2, 1 input, 2 outputs, locktime 0)
tx = transaction.Transaction(
    version=2,
    vin=[transaction.TransactionInput(
        txid=bytes.fromhex(PREV_TXID_HEX),
        vout=PREV_VOUT,
        sequence=0xFFFFFFFF,
    )],
    vout=[
        transaction.TransactionOutput(
            value=RECIPIENT_SATS,
            script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
        ),
        transaction.TransactionOutput(
            value=CHANGE_SATS,
            script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
        ),
    ],
    locktime=0,
)

# 4. Package into BIP 174 PSBT with Input Witness UTXO
psbt = PSBT(tx)
psbt.inputs[0].witness_utxo = transaction.TransactionOutput(
    value=INPUT_VALUE_SATS,
    script_pubkey=script.Script(bytes.fromhex(SENDER_SCRIPTPUBKEY_HEX)),
)

print(psbt.serialize().hex())
EOF

BTC_PSBT_HEX=$(python3 build_psbt.py)
echo "Generated PSBT Hex (${#BTC_PSBT_HEX} chars):"
echo "$BTC_PSBT_HEX"

🛑 Cas A : Refus (limite de frais dépassée)

Tenter d'autoriser une transaction dont les frais de minage dépassent la limite de 10 000 satoshis définie par la règle. Dans cet exemple, la définition de MINER_FEE_SATS = 15000 (et CHANGE_SATS = 135000) dans build_psbt.py passe à max_fee_atomic = 150000 (dépassant le plafond de la règle de 100000), ce qui produit la charge utile suivante qui déclenche une décision DENY du moteur de stratégies.

# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"

curl -X POST \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "principal": "User::\"alice\"",
    "action": "Action::\"Transfer\"",
    "resource": "Resource::\"mpc_operation\"",
    "schema": "BITCOIN_TX_V2",
    "payload": "'"$REJECTED_PSBT_HEX"'"
  }' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload | jq

Réponse attendue : decision: DENY

✅ Cas B : approbation (PSBT Bitcoin autorisé)

Tentez d'autoriser le PSBT valide précédemment construit (RECIPIENT_SATS = 50000 correspond à spend_amount_scaled = 500000 <= 2500000 et MINER_FEE_SATS = 250 correspond à max_fee_atomic = 2500 <= 100000).

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "principal": "User::\"alice\"",
    "action": "Action::\"Transfer\"",
    "resource": "Resource::\"mpc_operation\"",
    "schema": "BITCOIN_TX_V2",
    "payload": "'"$BTC_PSBT_HEX"'"
  }' \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine:authorizePayload)

echo "$RESPONSE" | jq

# Automatically capture variables for next steps
DIGEST_HEX=$(echo "$RESPONSE" | jq -r .digest)
POLICY_ENGINE_TOKEN=$(echo "$RESPONSE" | jq -r .token)

# Convert to Base64 (which curl expects for bytes fields)
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")
POLICY_ENGINE_TOKEN_B64=$(python3 -c "import base64; print(base64.b64encode('$POLICY_ENGINE_TOKEN'.encode('utf-8')).decode('utf-8'))")

Réponse attendue : decision: PERMIT avec un JWS token et un sighash BIP 143 de 32 octets digest.

Cette opération renvoie les valeurs :

  • decision : PERMIT ou DENY
  • token : jeton de la règle JWS (preuve de conformité).
  • digest : condensé de signature de 32 octets encodé en hexadécimal.
  • evaluatedPolicyIds : règles évaluées qui ont influencé la décision.

B. Créer une proposition (lancer la signature)

Proposez la charge utile du condensé pour signature, en joignant le jeton du moteur de stratégies reçu à l'étape 5A.

curl

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal"; then
  echo "Proposal 'my-proposal' already exists. Skipping creation."
else
  echo "Proposal 'my-proposal' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "digest": "'"$DIGEST_B64"'",
      "keyVersion": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/versions/1",
      "initiator": "projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice",
      "policyApproverSignature": "'"$POLICY_ENGINE_TOKEN_B64"'"
    }' \
  https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals?proposal_id=my-proposal | jq
fi

Cette opération renvoie une opération de longue durée (LRO). La proposition state attendue sera PENDING_ASSENT (indiquant qu'elle est prête à être signée par les participants).

🕵️‍♂️ Vérifier l'état de la proposition (interrogation)

Interrogez régulièrement l'état de la proposition en exécutant la commande suivante.

curl

curl -X GET \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq

C. Enregistrer les décisions des participants (consentement)

Les participants doivent signer le hachage de la proposition avec leurs clés privées locales (parts de clé) et transmettre l'approbation.

1. Signer le condensé de la proposition

Cloud KMS

sign_digest() {
  local signer_id=$1
  local digest_hex=$2

  local digest_b64=$(python3 -c "import base64, sys; print(base64.b64encode(bytes.fromhex('$digest_hex')).decode('utf-8'))")

  # Direct API calls are used here because the input is already a pre-computed 32-byte digest from the MPC.
  # Tools like gcloud assume input is raw data and apply a redundant hashing layer, altering the payload.
  # Unlike Policy signings which hash raw administrative text, Proposal signatures must sign this exact 32-byte hash as-is.
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "Content-Type: application/json" \
    -d "{\"digest\": {\"sha256\": \"$digest_b64\"}}" \
  https://cloudkms.googleapis.com/v1/projects/my-project/locations/us-central1/keyRings/mpc-assent-keys/cryptoKeys/${signer_id}-key/cryptoKeyVersions/1:asymmetricSign | jq -r .signature
}

SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")

OpenSSL

sign_digest() {
  local signer_id=$1
  local digest_hex=$2

  python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('$digest_hex'))" > digest.bin

  openssl pkeyutl -sign -inkey "${signer_id}_priv.pem" -in digest.bin -out digest_sig.bin

  openssl base64 -A -in digest_sig.bin
}

SIGNER_2_SIG_B64=$(sign_digest "signer-bob" "$DIGEST_HEX")
SIGNER_3_SIG_B64=$(sign_digest "signer-charlie" "$DIGEST_HEX")

2. Enregistrer la décision

curl

# 1. Check state for Signer 2
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")

PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
  echo "Recording assent for Signer 2..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "signature": "'"$SIGNER_2_SIG_B64"'",
      "decision": "ASSENT",
      "participant": "signer-bob"
    }' \
  https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
  echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 2."
else
  echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi

# 2. Re-check state for Signer 3 (Signer 2 might have triggered a state change)
PROPOSAL_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
  "https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal")

PROPOSAL_STATE=$(echo "$PROPOSAL_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$PROPOSAL_STATE" == "PENDING_ASSENT" ]; then
  echo "Recording assent for Signer 3..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "signature": "'"$SIGNER_3_SIG_B64"'",
      "decision": "ASSENT",
      "participant": "signer-charlie"
    }' \
  https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal:recordParticipantDecision | jq
elif [ "$PROPOSAL_STATE" == "READY" ] || [ "$PROPOSAL_STATE" == "SUCCEEDED" ]; then
  echo "Proposal is in state $PROPOSAL_STATE. Skipping Signer 3."
else
  echo "Proposal is in an unknown or unexpected state: $PROPOSAL_STATE"
fi

D. Récupérer la signature finale

Une fois le seuil d'approbations atteint, vérifiez l'état de la proposition ou récupérez la signature.

curl

curl -X GET \
  -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq

Si la proposition est à l'état SUCCEEDED, examinez le champ finalSignature dans le fichier JSON renvoyé.


Étape 6 : Diffuser la transaction sur la blockchain

Une fois que votre proposition a atteint l'état SUCCEEDED et que vous avez récupéré le finalSignature, la dernière étape consiste à construire la transaction filaire signée et à la diffuser sur le réseau.

Ethereum

Dérivation et diffusion de l'identifiant de récupération

L'API MPC renvoie une signature ECDSA brute de 64 octets (comprenant les valeurs r et s). Le serveur s'assure que la signature est au format canonique low-s (EIP-2). Cependant, Ethereum nécessite un identifiant de récupération (v) pour dériver l'adresse de l'expéditeur et vérifier la signature, qui n'est pas directement renvoyé dans les signatures MPC standards de 64 octets.

Le script Python suivant gère cette complexité. Il détermine de manière dynamique le bit de récupération correct en faisant correspondre l'adresse de l'expéditeur attendue, construit la transaction valide et la diffuse sur le réseau.

Script de l'annonce :

Exécutez le bloc suivant dans votre terminal pour écrire et exécuter broadcast_eth.py :

cat <<'EOF' > broadcast_eth.py
"""Broadcasts an Ethereum transaction using a normalized MPC signature."""

import base64
import os
import requests
from eth_account import Account
try:
    from eth_account._utils.transactions import encode_transaction, serializable_unsigned_transaction_from_dict
except ImportError:
    from eth_account._utils.legacy_transactions import encode_transaction, serializable_unsigned_transaction_from_dict

# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com"
EXPECTED_SENDER = "0xyour-mpc-key-address"

if not FINAL_SIGNATURE_B64 or FINAL_SIGNATURE_B64 == "your-final-signature":
    raise ValueError("FINAL_SIGNATURE_B64 is not set. Please provide a valid signature.")

# ==========================================
# 2. TRANSACTION PARAMETERS
# ==========================================
CHAIN_ID = 11155111  # Sepolia Testnet
tx_dict = {
    "nonce": int("0"),
    "gasPrice": int("20000000000"),  # 20 Gwei
    "gas": int("21000"),
    "to": "0x1111111111111111111111111111111111111111",
    "value": int("1000000000"),  # 1 Gwei
    "data": b"",
    "chainId": CHAIN_ID,
}

# ==========================================
# 3. DECODE & SPLIT SIGNATURE
# ==========================================
missing_padding = len(FINAL_SIGNATURE_B64) % 4
if missing_padding:
    FINAL_SIGNATURE_B64 += "=" * (4 - missing_padding)

sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
r = int.from_bytes(sig_bytes[:32], byteorder="big")
s = int.from_bytes(sig_bytes[32:], byteorder="big")

# ==========================================
# 4. RECOVERY IDENTIFIER (V) DERIVATION
# ==========================================
chain_v_base = (CHAIN_ID * 2) + 35

# ==========================================
# 5. GENERATE UNSIGNED TX & RECOVER SENDER ADDRESS
# ==========================================
unsigned_tx = serializable_unsigned_transaction_from_dict(tx_dict)

derived_sender = None
encoded_tx = None
final_v = chain_v_base

EXPECTED_SENDER_PLACEHOLDER = "0xyour-mpc-key-address"
is_valid_expected = EXPECTED_SENDER and EXPECTED_SENDER != EXPECTED_SENDER_PLACEHOLDER

for parity in [0, 1]:
    try_v = chain_v_base + parity
    try:
        tx_candidate = encode_transaction(unsigned_tx, vrs=(try_v, r, s))
        addr = Account.recover_transaction("0x" + tx_candidate.hex())

        if is_valid_expected and EXPECTED_SENDER.lower() == addr.lower():
            derived_sender = addr
            final_v = try_v
            encoded_tx = tx_candidate
            break

        if not is_valid_expected and derived_sender is None:
            derived_sender = addr
            final_v = try_v
            encoded_tx = tx_candidate
            print(f"Warning: EXPECTED_SENDER not provided or is placeholder. Assuming first recovered address: {addr}")

    except Exception:
        continue

if encoded_tx is None:
    if is_valid_expected:
        raise ValueError(f"Could not recover address matching EXPECTED_SENDER: {EXPECTED_SENDER}")
    else:
        raise ValueError("Could not derive broadcastable transaction. Ensure signature matches payload.")

if derived_sender:
    print(f"Derived Sender Address: {derived_sender}")
else:
    print("Derived Sender Address: Could not recover (ensure signature matches current transaction payload)")

valid_tx_hex = "0x" + encoded_tx.hex()
print(f"Broadcastable Transaction Hex:\n{valid_tx_hex}\n")

# ==========================================
# 6. BROADCAST TO NETWORK
# ==========================================
payload = {
    "jsonrpc": "2.0",
    "method": "eth_sendRawTransaction",
    "params": [valid_tx_hex],
    "id": 1,
}

print(f"Broadcasting to {RPC_URL}...")
response = requests.post(RPC_URL, json=payload)
resp_json = response.json()
print("RPC Response:", resp_json)
if "result" in resp_json:
    tx_hash = resp_json["result"]
    print(f"View transaction on Sepolia Etherscan: https://sepolia.etherscan.io/tx/{tx_hash}")
EOF

python3 broadcast_eth.py

Dépannage des erreurs courantes

  • invalid sender: recovery failed ou invalid transaction v, r, s values : cela se produit si les paramètres de transaction de votre script ne correspondent pas exactement à la charge utile signée. Assurez-vous que les champs value, nonce et to correspondent à ce qui a été traité à l'étape 5.
  • insufficient funds for gas * price + value : votre transaction est valide d'un point de vue cryptographique et a été acceptée par le nœud, mais l'adresse Ethereum de votre clé MPC ne dispose pas d'assez d'ETH testnet pour couvrir l'opération. Vous devrez approvisionner votre adresse avant de diffuser.

Bitcoin

Assemblage et diffusion des transactions SegWit Wire

Pour les transactions Bitcoin Native SegWit (BIP 141), la signature MPC (r, s) brute de 64 octets doit être formatée en ASN.1 DER canonique low-S, suivie de 0x01 (SIGHASH_ALL), et assemblée avec la clé publique compressée de 33 octets dans la pile de témoins SegWit à deux éléments : [DER signature || 0x01, compressed public key].

Script de l'annonce :

Exécutez le bloc suivant pour créer et exécuter broadcast_btc.py :

cat <<'EOF' > broadcast_btc.py
"""Assembles a signed Bitcoin Native SegWit transaction from an MPC signature using embit and broadcasts it."""

import base64
import urllib.request
from embit import ec, script, transaction

# ==========================================
# 1. INPUTS
# ==========================================
FINAL_SIGNATURE_B64 = "your-final-signature"
COMPRESSED_PUBKEY_HEX = "03deb76948c14003446a8c1d79e88fb3607c9bbc1f5143b186aa7b61d6b1facc36"

PREV_TXID_HEX = "8f594f33156993f1c980c2273dcae4a92cf2517fbc29264421395e63a63916d8"
PREV_VOUT = int("1")

RECIPIENT_SATS = int("50000")
RECIPIENT_SCRIPTPUBKEY_HEX = "0014751e76e8199196d454941c45d1b3a323f1433bd6"

CHANGE_SATS = int("149750")
CHANGE_SCRIPTPUBKEY_HEX = "0014eced05cd51d964102a20c856b09080ad620e9da2"

BROADCAST_API_URL = "https://mempool.space/testnet4/api/tx"

# ==========================================
# 2. CONVERT RAW (r, s) SIGNATURE TO CANONICAL LOW-S ASN.1 DER + SIGHASH_ALL
# ==========================================
sig_bytes = base64.b64decode(FINAL_SIGNATURE_B64)
internal_sig = ec.secp256k1.ecdsa_signature_parse_compact(sig_bytes)
ec.secp256k1.ecdsa_signature_normalize(internal_sig, internal_sig)
der_sig = ec.secp256k1.ecdsa_signature_serialize_der(internal_sig) + b"\x01"  # 0x01 = SIGHASH_ALL

# ==========================================
# 3. BUILD TRANSACTION & ATTACH SEGWIT WITNESS STACK
# ==========================================
tx = transaction.Transaction(
    version=2,
    vin=[transaction.TransactionInput(
        txid=bytes.fromhex(PREV_TXID_HEX),
        vout=PREV_VOUT,
        sequence=0xFFFFFFFF,
    )],
    vout=[
        transaction.TransactionOutput(
            value=RECIPIENT_SATS,
            script_pubkey=script.Script(bytes.fromhex(RECIPIENT_SCRIPTPUBKEY_HEX)),
        ),
        transaction.TransactionOutput(
            value=CHANGE_SATS,
            script_pubkey=script.Script(bytes.fromhex(CHANGE_SCRIPTPUBKEY_HEX)),
        ),
    ],
    locktime=0,
)

pubkey_bytes = bytes.fromhex(COMPRESSED_PUBKEY_HEX)
tx.vin[0].witness = transaction.Witness([der_sig, pubkey_bytes])

signed_tx_hex = tx.serialize().hex()
print("--- Signed Bitcoin SegWit Wire Transaction Hex ---")
print(signed_tx_hex)

# ==========================================
# 4. BROADCAST TO NETWORK
# ==========================================
if BROADCAST_API_URL:
  print(f"\nBroadcasting to {BROADCAST_API_URL}...")
  try:
    req = urllib.request.Request(
        BROADCAST_API_URL,
        data=signed_tx_hex.encode("utf-8"),
        headers={"Content-Type": "text/plain"},
        method="POST",
    )
    with urllib.request.urlopen(req) as resp:
      txid_result = resp.read().decode("utf-8").strip()
      print(f"✅ Broadcast Successful! Transaction ID (txid): {txid_result}")
      print(f"Track in Mempool Explorer: https://mempool.space/testnet4/tx/{txid_result}")
  except Exception as e:
    print(f"Broadcast response: {e}")
EOF

python3 broadcast_btc.py

Dépannage des erreurs courantes

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature) : assurez-vous que le composant s de la signature est au format canonique low-s (s <= N/2) et correctement encodé en DER.
  • bad-txns-in-belowout / bad-txns-fee-negative : le total des sorties dépasse la valeur d'entrée. Assurez-vous que le calcul de la variation (InputSatoshis - PaymentSatoshis - MinerFee) est strictement positif.

📖 Annexe : Guide opérationnel (interrogation et inspection)

Utilisez ces commandes en lecture seule pour inspecter l'état de vos ressources à tout moment.


👥 1. Participants et signataires

Inspectez les participants et les signataires de votre instance.

A. Lister tous les participants inscrits

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers | jq

B. Obtenir des informations sur un participant spécifique

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/signers/signer-alice | jq

📜 2. moteur de stratégies

Inspectez les règles et les bundles de confiance.

A. Obtenir le bundle de confiance de l'auteur de la stratégie

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policyAuthorTrustBundle | jq

B. Lister toutes les règles

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies | jq

C. Obtenir des détails sur une stratégie spécifique

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/policyEngine/policies/ethereum-transfer-policy | jq

🔑 3. Clés et trousseaux de clés

Inspecter les trousseaux de clés et les clés.

A. Lister tous les trousseaux de clés

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings | jq

B. Répertorier toutes les clés d'un trousseau de clés

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys | jq

C. Obtenir l'état d'une clé distribuée spécifique (état DKG)

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key | jq

📝 4. Propositions (opérations de signature)

Inspectez les propositions et l'état de signature.

A. Lister toutes les propositions pour une clé

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals | jq

B. Obtenir des détails ou l'état d'une proposition spécifique

curl

curl -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \
  -H "X-Goog-User-Project: my-project" \
https://67317436-0f29-ba9e-9146-d11c0bd8df9b.us-central1.mpc.goog/v1alpha1/projects/my-project/locations/us-central1/instances/mpc-unit-bob-123/mpc/keyRings/my-key-ring/keys/my-key/proposals/my-proposal | jq