Guía del usuario de Multi-party Compute

En esta guía, se describe cómo usar la API de Multi-party Compute para administrar claves, participantes y políticas distribuidos. Está destinado a los clientes de la versión preliminar que están aprendiendo los recorridos del usuario clave.

Descripción general

La API de MPC consta de dos componentes principales:

  1. Core MPC administra el ciclo de vida de las claves distribuidas, las propuestas y las ceremonias de firma.
  2. Policy Engine aplica la gobernanza y autoriza las operaciones antes de que se ejecuten.

Para obtener un desglose detallado de cómo interactúan el acceso a la API y la firma criptográfica, consulta Seguridad y autorización.

Requisitos previos

Antes de comenzar, asegúrate de tener lo siguiente:

  1. Un proyecto de Google Cloud . Consulta Crea proyectos para crear uno.
  2. Si no usas Cloud Shell, la CLI de gcloud debe estar instalada y autenticada.
  3. Las APIs requeridas deben estar habilitadas, si aún no lo están. Necesitas dos: la nueva API de MPC que se usa en esta guía y Cloud Key Management Service si lo usas para generar o firmar claves de consentimiento.

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

    Para ver las APIs habilitadas, sigue estos pasos:

      gcloud services list --enabled
    
  4. Python 3.9 o una versión posterior y las bibliotecas auxiliares del instructivo instaladas:

      pip install embit eth-account eth-utils requests
    
  5. (Opcional) Si tu organización se autentica con un proveedor de identidad externo (como Okta o Microsoft Entra ID) sin Cuentas de Google, consulta Configura la federación de identidades de personal (BYOID) para configurar las credenciales predeterminadas de la aplicación (ADC) con tu identidad federada.

Configuración

Actualiza las siguientes variables para propagar los comandos en esta guía.

Configuración obligatoria

Debes personalizar estos parámetros. La guía no funcionará con los valores de marcador de posición.

Parámetro Valor Descripción
ID del proyecto my-project Es el ID del proyecto de consumidor que nos proporcionaste para la incorporación a la versión preliminar.
ID del dominio 67317436-0f29-ba9e-9146-d11c0bd8df9b Es el ID de dominio único que asignamos a tu instancia.
ID de la ruta de la instancia mpc-unit-bob-123 Es el ID de la instancia de ruta que asignamos a tu instancia.
Ubicación us-central1 Ubicación de la Deployment. Debe ser us-central1 (no se admiten otras regiones en la versión preliminar).

Etiquetas de recursos del instructivo

Estos son los nombres de los recursos creados durante este instructivo. Los valores predeterminados son válidos y se pueden mantener para minimizar las dificultades de configuración.

Parámetro Valor Descripción
ID del llavero de claves de Cloud KMS mpc-assent-keys Es el llavero de claves de Cloud Key Management Service para las claves de certificación externas almacenadas en Cloud KMS.
ID del firmante 1 signer-alice Firmante 1 de MPC.
ID del firmante 2 signer-bob Firmante 2 de MPC.
ID del firmante 3 signer-charlie Firmante 3 de MPC.
ID del autor de la política 1 author-dave Autor de la política 1.
ID del autor de la política 2 author-eve Autor de políticas 2.
ID de la política ethereum-transfer-policy ID de la política (por ejemplo, ethereum-transfer-policy o bitcoin-psbt-policy)
ID del llavero de claves my-key-ring ID del llavero de claves
ID de clave my-key ID de clave
ID de la propuesta my-proposal ID de la propuesta.

Paso 1: Genera claves de consentimiento

Para usar la API de MPC, debes generar pares de claves para los siguientes roles distintos. Ten en cuenta que estas son claves de consentimiento criptográfico externas y son distintas de los fragmentos de claves de umbral de MPC distribuidos que se generan de forma interna durante la configuración.

  • Autores de políticas: Autorizan las políticas de administración.
  • Firmantes: Brindan su consentimiento (votan o aprueban) en las propuestas.

A. Claves de participante (secp256k1)

Genera las claves criptográficas necesarias para tus participantes. Estos comandos son idempotentes.

Cloud KMS

Te recomendamos que administres las claves de consentimiento en Cloud Key Management Service. Asegúrate de que tu identidad tenga permisos para crear llaveros de claves, crear claves, firmar y ver verificadores de claves públicas (como roles/cloudkms.admin y roles/cloudkms.signerVerifier). Asegúrate de tener un KeyRing creado y, luego, genera una clave secp256k1 para todos tus firmantes.

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

Puedes usar OpenSSL estándar para generar claves secp256k1 para cada participante.

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. Claves de autor de la política (secp256k1)

Cloud KMS

Del mismo modo, te recomendamos que administres estas claves de consentimiento en Cloud Key Management Service. Asegúrate de que tu identidad tenga permisos para crear llaveros de claves, crear claves, firmar y ver verificadores de claves públicas (como roles/cloudkms.admin y roles/cloudkms.signerVerifier). Asegúrate de tener un KeyRing creado y, luego, genera una clave secp256k1 para todos tus autores de políticas.

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

Los autores de políticas usan la curva secp256k1 para firmar políticas de administración y acciones administrativas. Puedes generar estas claves con OpenSSL estándar.

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

Paso 2: Registra a los participantes

Antes de que cualquier entidad pueda participar en el flujo de MPC (como firmante) o en el flujo de políticas (como autor de políticas), debe registrarse como participante.

El registro requiere una coordinación fuera de banda entre el administrador y el participante:

  1. La entidad (firmante o autor) genera su par de claves de forma local y transfiere su clave pública al administrador de forma segura.
  2. El administrador (que tiene permisos de IAM Google Cloud estándar) llama a la API para registrar la clave pública.

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

Paso 3: Configura Policy Engine

Configura el Policy Engine estableciendo el paquete de confianza y creando políticas.

A. Inicializa el paquete de confianza del autor de la política

Define quiénes son los autores de políticas autorizados. Este es un recurso singleton para la instancia.

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. Crea una política (PENDING_ACTIVE)

Define un ID de política (ethereum-transfer-policy o bitcoin-psbt-policy) y crea una regla que describa qué acciones se permiten. Si bien puedes configurar muchas políticas para controlar diferentes situaciones, en esta guía, se muestra cómo configurar una política para transacciones de Ethereum o Bitcoin. De forma predeterminada, el Policy Engine tiene la opción Fail-Fast (Block by Default). Si ninguna política permite explícitamente una transacción, esta se rechaza.

Antes de crear o aprobar una política, los autores deben firmar una carga útil que represente la intención. Puedes usar las siguientes funciones auxiliares para controlar la serialización y la firma binarias.

Ethereum

Esta regla permite transferencias de Ethereum más pequeñas (<= 2 ETH, representadas como 2,000,000,000 Gwei) a una dirección específica para Alice.

1. Generar firma de quórum

Usa 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")

Con 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. Llama a la API de 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

Esta regla permite las transferencias de Bitcoin con el esquema bitcoin.transaction.v2 (BIP 174 PSBT) en las que el importe neto de la transferencia saliente no supera los 250,000 satoshis (context.transaction.spend_amount_scaled <= 2500000, o 0.0025 BTC normalizados a 10-9 unidades decimales fijas) y la comisión del minero no supera los 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. Generar firma de quórum

Usa 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")

Con 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. Llama a la API de 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. Aprobar la política (transición a ACTIVE)

Si la configuración de tu política requiere firmas M de N, los autores posteriores llaman a AddQuorumSignature.

Para generar la firma, primero debes recuperar la secuencia actual de la política.

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

Paso 4: Crea una clave

Ahora, crearemos un llavero de claves y generaremos una clave distribuida.

A. Crear llavero de claves

Agrupación lógica para tus claves de 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. Genera una clave distribuida (DKG)

Inicia una ceremonia asíncrona para generar una clave distribuida entre los participantes.

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

Esto devuelve una operación de larga duración (LRO). Debes esperar a que la clave state se convierta en ACTIVE (lo que significa que la ceremonia de DKG se realizó correctamente).

🕵️‍♂️ Verifica el estado de la clave (sondeo)

Sondea el estado de la clave ejecutando el siguiente comando de forma periódica hasta que el campo state de la respuesta se convierta en ACTIVE (lo que indica que la ceremonia de DKG se realizó correctamente).

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. Deriva la dirección de la cadena de bloques y ubica las UTXO

Una vez que tu clave distribuida se convierta en ACTIVE, la clave de MPC estará lista para usarse. En este paso, derivarás la dirección específica de la cadena de bloques a partir de la clave pública sin comprimir de 65 bytes que devuelve la generación de claves de MPC (0x04 || X || Y) y ubicarás los fondos no gastados necesarios para las transacciones.

Ethereum

Las direcciones de Ethereum se derivan del hash Keccak-256 del punto de clave pública sin comprimir de 64 bytes (pub_bytes[1:]), tomando los últimos 20 bytes.

Secuencia de comandos de derivación de direcciones (derive_address.py)

Ejecuta el siguiente bloque para crear y ejecutar 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

Financia la dirección de Ethereum

Antes de transmitir en el paso 6, asegúrate de que tu dirección de Ethereum derivada tenga suficiente ETH de la red de prueba Sepolia para cubrir el valor de la transacción y los costos de gas. Puedes solicitar ETH de testnet gratis ingresando tu dirección recién derivada en el Faucet para Ethereum Sepolia.

Bitcoin

Las direcciones de Bitcoin se derivan de la clave pública de EC secp256k1 de 65 bytes sin comprimir (0x04 || X || Y). Se admiten varios formatos de dirección de la misma clave pública:

  1. SegWit nativo (P2WPKH / BIP 173): Dirección Bech32 (bc1q... en Mainnet, tb1q... en Testnet4) con un testigo de 22 bytes scriptPubKey (0014<hash160>). Este es el formato moderno recomendado con las comisiones por transacción más bajas.
  2. Nested SegWit (P2SH-P2WPKH / BIP 49): Dirección de SegWit encapsulada (3... en Mainnet, 2... en Testnet4) con un scriptPubKey (a914<hash160>87) de 23 bytes y un redeemScript (0014<hash160>) de 22 bytes, compatible con sistemas heredados.
  3. Heredada (P2PKH / BIP 44): Dirección Base58 original de Pay-to-PubKey-Hash (1... en Mainnet, m/n... en Testnet4) con un scriptPubKey de 25 bytes (76a914<hash160>88ac).

Secuencia de comandos de derivación de direcciones (derive_btc_address.py)

Ejecuta el siguiente bloque para crear y ejecutar 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

Localización de salidas no gastadas (UTXO)

A diferencia de las cadenas de bloques basadas en cuentas, las transacciones de Bitcoin gastan salidas de transacciones no gastadas (UTXO) específicas. Antes de construir una transacción en el paso 5, haz lo siguiente:

  1. Financia tu dirección de SegWit nativo derivada (por ejemplo, con un faucet de testnet).
  2. Consulta los UTXO no gastados de tu dirección con tu nodo de Bitcoin (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") o una API de explorador de bloques.
  3. Ten en cuenta los siguientes 4 parámetros para el paso 5:
    • txid: Es el hash de transacción de 32 bytes de la salida no gastada.
    • vout: Es el índice de salida con índice cero (por ejemplo, 0 o 1).
    • value: Es el importe no gastado en satoshis (por ejemplo, 200000 satoshis = 0.002 BTC).
    • scriptPubKey: Es el hexadecimal de la secuencia de comandos de bloqueo (por ejemplo, 0014...).

Paso 5: Ejecuta una operación de MPC

Ejecuta una operación de MPC evaluando una transacción y creando una propuesta.

A. Evalúa la transacción (obtén el token de autorización)

Antes de firmar una operación de MPC, autoriza la carga útil con tus políticas para recibir un token de JWS y el resumen de firma de 32 bytes.

Ethereum

🛑 Caso A: Rechazo (valor demasiado alto)

Intenta autorizar una transacción de 3 ETH (supera nuestro límite 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

Respuesta esperada: decision: DENY

✅ Caso B: Aprobación (valor permitido)

Intenta autorizar una transacción de 1 Gwei (dentro de nuestro límite 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'))")

Respuesta esperada: decision: PERMIT junto con un token.

Bitcoin

En el modelo UTXO de Bitcoin, las transacciones gastan salidas específicas no gastadas. En SegWit (BIP 143), calcular el hash de la firma requiere conocer la cantidad de satoshis y el script de bloqueo de la entrada que se está gastando.

El Policy Engine requiere el formato BIP 174 PSBT v0 (BITCOIN_TX_V2) con PSBT_IN_WITNESS_UTXO completado para cada entrada.

Secuencia de comandos de construcción de PSBT (build_psbt.py)

Ejecuta el siguiente bloque para ensamblar una carga útil de PSBT v0 de BIP 174 válida:

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"

🛑 Caso A: Rechazo (se superó el límite de tarifas)

Intentar autorizar una transacción en la que la comisión del minero supere el límite de la política de 10,000 satoshis En este ejemplo, configurar MINER_FEE_SATS = 15000 (y CHANGE_SATS = 135000) en build_psbt.py escala a max_fee_atomic = 150000 (lo que supera el límite de la política de 100000), lo que produce la siguiente carga útil que activa una decisión de DENY del Policy Engine.

# 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

Respuesta esperada: decision: DENY

✅ Caso B: Aprobación (PSBT de Bitcoin permitida)

Intenta autorizar la PSBT válida construida anteriormente (RECIPIENT_SATS = 50000 se asigna a spend_amount_scaled = 500000 <= 2500000 y MINER_FEE_SATS = 250 se asigna a 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'))")

Respuesta esperada: decision: PERMIT junto con un JWS token y un sighash BIP 143 de 32 bytes digest.

El resultado es el siguiente:

  • decision: PERMIT o DENY
  • token: Es el token de la política de JWS (prueba de cumplimiento).
  • digest: Es el resumen de firma de 32 bytes codificado en hexadecimal.
  • evaluatedPolicyIds: Son las políticas que se evaluaron y que afectaron la decisión.

B. Crear propuesta (iniciar firma)

Ofrece la carga útil del resumen para la firma y adjunta el token de Policy Engine recibido en el paso 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

Esto devuelve una operación de larga duración (LRO). El estado esperado de la propuesta state será PENDING_ASSENT (lo que indica que está lista para que la firmen los participantes).

🕵️‍♂️ Verifica el estado de la propuesta (sondeo)

Sondea el estado de la propuesta ejecutando el siguiente comando de forma periódica.

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. Registra las decisiones de los participantes (asentimiento)

Los participantes deben firmar el hash de la propuesta con sus claves privadas locales (fragmentos de clave) y reenviar el consentimiento.

1. Firmar el resumen de la propuesta

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. Registra la decisión

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. Recuperar firma final

Una vez que se alcance el umbral de aprobaciones, verifica el estado de la propuesta o recupera la firma.

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 propuesta está en estado SUCCEEDED, consulta el campo finalSignature dentro del JSON que se muestra.


Paso 6: Transmite la transacción a la cadena de bloques

Una vez que tu propuesta alcance el estado SUCCEEDED y hayas recuperado el finalSignature, el último paso es construir la transacción de transferencia firmada y transmitirla a la red.

Ethereum

Derivación y transmisión del identificador de recuperación

La API de MPC devuelve una firma ECDSA sin procesar de 64 bytes (que comprende los valores r y s). El servidor garantiza que la firma esté en formato canónico low-s (EIP-2). Sin embargo, Ethereum requiere un identificador de recuperación (v) para derivar la dirección del remitente y verificar la firma, que no se devuelve directamente en las firmas MPC estándar de 64 bytes.

La siguiente secuencia de comandos de Python controla esta complejidad. Determina de forma dinámica el bit de recuperación correcto haciendo coincidir la dirección del remitente esperada, construye la transacción válida y la transmite a la red.

Guion de la transmisión:

Ejecuta el siguiente bloque que se puede copiar y pegar en tu terminal para escribir y ejecutar 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

Solución de problemas comunes

  • invalid sender: recovery failed o invalid transaction v, r, s values: Esto ocurre si los parámetros de transacción de tu secuencia de comandos no coinciden exactamente con la carga útil que se firmó. Asegúrate de que los campos value, nonce y to coincidan con lo que se procesó en el paso 5.
  • insufficient funds for gas * price + value: Tu transacción es criptográficamente válida y el nodo la aceptó, pero la dirección de Ethereum de tu clave de MPC no tiene suficientes ETH de testnet para cubrir la operación. Deberás depositar fondos en tu dirección antes de transmitir.

Bitcoin

Ensamblaje y transmisión de transacciones de SegWit

Para las transacciones de SegWit nativo de Bitcoin (BIP 141), la firma de MPC de (r, s) de 64 bytes sin procesar debe tener el formato ASN.1 DER canónico de S bajo, se debe agregar con 0x01 (SIGHASH_ALL) y se debe ensamblar con la clave pública comprimida de 33 bytes en la pila de testigos de SegWit de 2 elementos: [DER signature || 0x01, compressed public key].

Guion de la transmisión:

Ejecuta el siguiente bloque para crear y ejecutar 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

Solución de problemas comunes

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature): Asegúrate de que el componente s de la firma esté en formato canónico low-s (s <= N/2) y codificado correctamente en DER.
  • bad-txns-in-belowout / bad-txns-fee-negative: El total de salidas supera el valor de entrada. Asegúrate de que el cálculo del cambio (InputSatoshis - PaymentSatoshis - MinerFee) sea estrictamente positivo.

📖 Apéndice: Guía operativa (consultas e inspecciones)

Usa estos comandos de solo lectura para inspeccionar el estado de tus recursos en cualquier momento.


👥 1. Participantes y firmantes

Inspecciona los participantes y firmantes de tu instancia.

A. Enumera todos los participantes registrados

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. Obtén detalles de un participante específico

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. Policy Engine

Inspeccionar políticas y paquetes de confianza

A. Obtén el paquete de confianza del autor de la política

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. Enumera todas las políticas

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. Obtén detalles de una política específica

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. Claves y llaveros de claves

Inspeccionar llaveros de claves y claves

A. Enumera todos los llaveros de claves

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. Enumera todas las claves de un llavero de claves

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. Obtén el estado de una clave distribuida específica (estado de 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. Propuestas (operaciones de firma)

Inspeccionar las propuestas y el estado de firma

A. Enumera todas las propuestas de una clave

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. Obtén detalles o el estado de una propuesta específica

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