Guida dell'utente di Multi-party Compute

Questa guida descrive come utilizzare l'API Multi-party Compute per gestire chiavi, partecipanti e policy distribuiti. È destinato ai clienti in anteprima che stanno imparando i percorsi degli utenti principali.

Panoramica

L'API MPC è composta da due componenti principali:

  1. Core MPC gestisce il ciclo di vita delle chiavi distribuite, delle proposte e delle cerimonie di firma.
  2. Policy Engine applica la governance, autorizzando le operazioni prima che vengano eseguite.

Per un'analisi dettagliata dell'interazione tra l'accesso alle API e la firma crittografica, consulta Sicurezza e autorizzazione.

Prerequisiti

Prima di iniziare, assicurati di avere:

  1. Un progetto Google Cloud . Consulta Crea progetti per crearne uno.
  2. Se non utilizzi Cloud Shell, l'interfaccia a riga di comando gcloud installata e autenticata.
  3. Abilitazione delle API richieste, se non è già stato fatto. Ne servono due: la nuova API MPC utilizzata in questa guida e Cloud Key Management Service, se lo utilizzi per la generazione/firma delle chiavi di consenso.

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

    Per visualizzare le API abilitate:

      gcloud services list --enabled
    
  4. Python 3.9+ e le librerie di supporto del tutorial installate:

      pip install embit eth-account eth-utils requests
    
  5. (Facoltativo) Se la tua organizzazione esegue l'autenticazione utilizzando un provider di identità esterno (come Okta o Microsoft Entra ID) senza Account Google, consulta Configura la federazione delle identità per la forza lavoro (BYOID) per configurare le Credenziali predefinite dell'applicazione (ADC) con la tua identità federata.

Configurazione

Aggiorna le seguenti variabili per compilare i comandi in questa guida.

Configurazione richiesta

Devi personalizzare questi parametri. La guida non funzionerà con i valori segnaposto.

Parametro Valore Descrizione
ID progetto my-project L'ID progetto consumer che ci hai fornito per l'onboarding nell'anteprima.
ID dominio 67317436-0f29-ba9e-9146-d11c0bd8df9b L'ID dominio univoco che abbiamo assegnato alla tua istanza.
ID percorso istanza mpc-unit-bob-123 L'ID istanza del percorso che abbiamo assegnato alla tua istanza.
Località us-central1 Località di deployment. Deve essere us-central1 (le altre regioni non sono supportate in anteprima).

Etichette delle risorse del tutorial

Questi sono i nomi delle risorse create durante questo tutorial. I valori predefiniti sono validi e possono essere mantenuti per ridurre al minimo le difficoltà di configurazione.

Parametro Valore Descrizione
ID keyring Cloud KMS mpc-assent-keys KeyRing di Cloud Key Management Service per le chiavi di assenso esterne archiviate in Cloud KMS.
ID firmatario 1 signer-alice Firmatario MPC 1.
ID firmatario 2 signer-bob Firmatario MPC 2.
ID firmatario 3 signer-charlie Firmatario MPC 3.
ID autore policy 1 author-dave Autore della policy 1.
ID autore della policy 2 author-eve Autore delle norme 2.
ID policy ethereum-transfer-policy ID policy (ad esempio, ethereum-transfer-policy o bitcoin-psbt-policy).
ID keyring my-key-ring ID KeyRing.
ID chiave my-key ID chiave.
ID proposta my-proposal ID proposta.

Passaggio 1: genera le chiavi di consenso

Per utilizzare l'API MPC, devi generare coppie di chiavi per i seguenti ruoli distinti. Tieni presente che si tratta di chiavi di consenso crittografiche esterne, distinte dagli shard di chiavi di soglia MPC distribuite generati internamente durante la configurazione.

  • Autori delle policy: autorizzano le policy di governance.
  • Firmatari: forniscono il consenso (voto/approvazione) alle proposte.

A. Chiavi dei partecipanti (secp256k1)

Genera le chiavi di crittografia necessarie per i partecipanti. Questi comandi sono idempotenti.

Cloud KMS

Ti consigliamo di gestire le chiavi di consenso in Cloud Key Management Service. Assicurati che la tua identità disponga delle autorizzazioni per creare chiavi automatizzate, creare chiavi, firmare e visualizzare i verificatori di chiavi pubbliche (come roles/cloudkms.admin e roles/cloudkms.signerVerifier). Assicurati di aver creato un KeyRing, quindi genera una chiave secp256k1 per tutti i firmatari.

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

Puoi utilizzare OpenSSL standard per generare chiavi secp256k1 per ogni partecipante.

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. Chiavi dell'autore della policy (secp256k1)

Cloud KMS

Analogamente, ti consigliamo di gestire queste chiavi di consenso in Cloud Key Management Service. Assicurati che la tua identità disponga delle autorizzazioni per creare chiavi automatizzate, creare chiavi, firmare e visualizzare i verificatori di chiavi pubbliche (come roles/cloudkms.admin e roles/cloudkms.signerVerifier). Assicurati di aver creato un KeyRing, quindi genera una chiave secp256k1 per tutti gli autori delle policy.

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

Gli autori delle policy utilizzano la curva secp256k1 per firmare le policy di governance e le azioni amministrative. Puoi generare queste chiavi utilizzando 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

Passaggio 2: registra i partecipanti

Prima che un'entità possa partecipare al flusso MPC (come firmatario) o al flusso delle policy (come autore delle policy), deve essere registrata come partecipante.

La registrazione richiede un coordinamento out-of-band tra l'Amministratore e il Partecipante:

  1. L'entità (firmatario o autore) genera la propria coppia di chiavi localmente e trasferisce la propria chiave pubblica all'amministratore in modo sicuro.
  2. L'amministratore (con autorizzazioni IAM Google Cloud standard) chiama l'API per registrare la chiave pubblica.

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

Passaggio 3: configura Policy Engine

Configura il Policy Engine impostando il bundle di attendibilità e creando le policy.

A. Inizializza il bundle di attendibilità dell'autore delle policy

Definisci chi sono gli autori delle policy autorizzati. Si tratta di una risorsa singleton per l'istanza.

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 policy (PENDING_ACTIVE)

Definisci un ID policy (ethereum-transfer-policy o bitcoin-psbt-policy) e crea una regola che descriva le azioni consentite. Sebbene sia possibile configurare molte policy per gestire scenari diversi, questa guida illustra la configurazione di una policy per le transazioni Ethereum o Bitcoin. Per impostazione predefinita, il Policy Engine è Fail-Fast (Block by Default); se nessuna policy consente esplicitamente una transazione, questa viene negata.

Prima di creare o approvare una policy, gli autori devono firmare un payload che rappresenta l'intent. Puoi utilizzare le seguenti funzioni helper per gestire la serializzazione e la firma binarie.

Ethereum

Questa regola consente trasferimenti di Ethereum di importo inferiore (<= 2 ETH, rappresentati come 2.000.000.000 Gwei) a un indirizzo specifico per Alice.

1. Genera firma del quorum

Utilizzo di 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")

Utilizzo di 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. Chiama 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

Questa regola consente i trasferimenti di Bitcoin utilizzando lo schema bitcoin.transaction.v2 (BIP 174 PSBT) in cui l'importo netto del trasferimento in uscita non supera i 250.000 satoshi (context.transaction.spend_amount_scaled <= 2500000 o 0,0025 BTC normalizzati a 10-9 unità decimali fisse) e la commissione del miner non supera i 10.000 satoshi (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. Genera firma del quorum

Utilizzo di 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")

Utilizzo di 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. Chiama 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. Approva la policy (passaggio ad ATTIVA)

Se la configurazione della policy richiede firme M-of-N, gli autori successivi chiamano AddQuorumSignature.

Per generare la firma, devi prima recuperare la sequenza corrente della policy.

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

Passaggio 4: crea una chiave

Ora creiamo chiavi automatizzate e generiamo una chiave distribuita.

A. Crea KeyRing

Raggruppamento logico per le chiavi 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 chiave distribuita (DKG)

Avvia una cerimonia asincrona per generare una chiave distribuita tra i partecipanti.

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

Viene restituita un'operazione a lunga esecuzione (LRO). Devi attendere che la chiave state diventi ACTIVE (il che significa che la cerimonia DKG è riuscita).

🕵️‍♂️ Controlla lo stato della chiave (polling)

Esegui periodicamente il polling dello stato della chiave eseguendo il comando seguente finché il campo state nella risposta non diventa ACTIVE (a indicare che la cerimonia DKG è riuscita).

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 l'indirizzo blockchain e individua gli UTXO

Quando la chiave distribuita diventa ACTIVE, la chiave MPC è pronta per l'uso. In questo passaggio, ricavi l'indirizzo specifico della blockchain dalla chiave pubblica non compressa di 65 byte restituita dalla generazione della chiave MPC (0x04 || X || Y) e individui eventuali fondi non spesi necessari per le transazioni.

Ethereum

Gli indirizzi Ethereum sono derivati dall'hash Keccak-256 del punto della chiave pubblica non compressa di 64 byte (pub_bytes[1:]), prendendo gli ultimi 20 byte.

Script di derivazione dell'indirizzo (derive_address.py)

Esegui il seguente blocco per creare ed eseguire 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

Finanzia l'indirizzo Ethereum

Prima di trasmettere nel Passaggio 6, assicurati che il tuo indirizzo Ethereum derivato abbia abbastanza ETH della rete di test Sepolia per coprire il valore della transazione e i costi del gas. Puoi richiedere ETH di testnet senza costi inserendo l'indirizzo appena derivato nel Faucet per Ethereum Sepolia.

Bitcoin

Gli indirizzi Bitcoin derivano dalla chiave pubblica EC secp256k1 non compressa da 65 byte (0x04 || X || Y). Sono supportati più formati di indirizzo dalla stessa chiave pubblica:

  1. Native SegWit (P2WPKH / BIP 173): indirizzo Bech32 (bc1q... su Mainnet, tb1q... su Testnet4) con un witness di 22 byte scriptPubKey (0014<hash160>). Questo è il formato moderno consigliato con le commissioni di transazione più basse.
  2. Nested SegWit (P2SH-P2WPKH / BIP 49): indirizzo SegWit con wrapping (3... su Mainnet, 2... su Testnet4) con un scriptPubKey (a914<hash160>87) di 23 byte e un redeemScript (0014<hash160>) di 22 byte, compatibile con i sistemi legacy.
  3. Legacy (P2PKH/BIP 44): indirizzo Base58 Pay-to-PubKey-Hash originale (1... su Mainnet, m/n... su Testnet4) con un scriptPubKey (76a914<hash160>88ac) di 25 byte.

Script di derivazione dell'indirizzo (derive_btc_address.py)

Esegui il seguente blocco per creare ed eseguire 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

Individuazione degli output non spesi (UTXO)

A differenza delle blockchain basate su account, le transazioni Bitcoin spendono specifici output di transazioni non spesi (UTXO). Prima di creare una transazione nel Passaggio 5:

  1. Finanzia l'indirizzo Native SegWit derivato (ad esempio, utilizzando un faucet testnet).
  2. Esegui una query sugli UTXO non spesi del tuo indirizzo utilizzando il tuo nodo Bitcoin (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") o un'API di esplorazione dei blocchi.
  3. Prendi nota dei seguenti 4 parametri per il passaggio 5:
    • txid: l'hash della transazione di 32 byte dell'output non speso.
    • vout: l'indice di output con indicizzazione zero (ad esempio 0 o 1).
    • value: l'importo non speso in satoshi (ad esempio, 200000 satoshi = 0,002 BTC).
    • scriptPubKey: il valore esadecimale dello script di blocco (ad esempio, 0014...).

Passaggio 5: esegui un'operazione MPC

Esegui un'operazione MPC valutando una transazione e creando una proposta.

A. Valuta transazione (Ottieni token di autorizzazione)

Prima di firmare un'operazione MPC, autorizza il payload in base alle tue policy per ricevere un token JWS e il digest di firma di 32 byte.

Ethereum

🛑 Caso A: rifiuto (valore troppo alto)

Prova ad autorizzare una transazione di 3 ETH (supera il nostro limite di 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

Risposta prevista: decision: DENY

✅ Caso B: approvazione (valore consentito)

Prova ad autorizzare una transazione di 1 Gwei (entro il nostro limite di 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'))")

Risposta prevista: decision: PERMIT insieme a token.

Bitcoin

Nel modello UTXO di Bitcoin, le transazioni spendono output non spesi specifici. In SegWit (BIP 143), il calcolo dell'hash della firma richiede la conoscenza dell'importo in satoshi e dello script di blocco dell'input speso.

Il Policy Engine richiede il formato BIP 174 PSBT v0 (BITCOIN_TX_V2) con PSBT_IN_WITNESS_UTXO compilato per ogni input.

Script di costruzione PSBT (build_psbt.py)

Esegui il seguente blocco per assemblare un payload PSBT v0 BIP 174 valido:

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: rifiuto (limite di commissione superato)

Tentativo di autorizzare una transazione in cui la commissione per i miner supera il limite di policy di 10.000 satoshi. In questo esempio, l'impostazione di MINER_FEE_SATS = 15000 (e CHANGE_SATS = 135000) in build_psbt.py scala fino a max_fee_atomic = 150000 (superando il limite della policy di 100000), producendo il seguente payload che attiva una decisione DENY dal 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

Risposta prevista: decision: DENY

✅ Caso B: approvazione (PSBT Bitcoin consentita)

Tenta di autorizzare la PSBT valida creata in precedenza (RECIPIENT_SATS = 50000 corrisponde a spend_amount_scaled = 500000 <= 2500000 e MINER_FEE_SATS = 250 corrisponde 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'))")

Risposta prevista: decision: PERMIT insieme a un JWS token e un sighash BIP 143 da 32 byte digest.

Che restituisce:

  • decision: PERMIT o DENY
  • token: il token della policy JWS (prova di conformità).
  • digest: il digest di firma di 32 byte con codifica esadecimale.
  • evaluatedPolicyIds: le policy valutate che hanno influito sulla decisione.

B. Crea proposta (avvia la firma)

Offri il payload del digest per la firma, allegando il token del Policy Engine ricevuto nel passaggio 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

Viene restituita un'operazione a lunga esecuzione (LRO). La proposta state prevista sarà PENDING_ASSENT (a indicare che è pronta per essere firmata dai partecipanti).

🕵️‍♂️ Controlla lo stato della proposta (polling)

Esegui periodicamente il polling dello stato della proposta eseguendo questo comando.

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 le decisioni dei partecipanti (assenso)

I partecipanti devono firmare l'hash della proposta con le proprie chiavi private locali (Key Share) e inoltrare l'approvazione.

1. Firma il digest della proposta

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 decisione

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. Recupera firma finale

Una volta raggiunta la soglia di approvazioni, controlla lo stato della proposta 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

Se la proposta è in stato SUCCEEDED, esamina il campo finalSignature all'interno del JSON restituito.


Passaggio 6: trasmetti la transazione alla blockchain

Una volta che la proposta raggiunge lo stato SUCCEEDED e hai recuperato finalSignature, l'ultimo passaggio consiste nel creare la transazione wire firmata e trasmetterla alla rete.

Ethereum

Derivazione e trasmissione dell'identificatore di recupero

L'API MPC restituisce una firma ECDSA non elaborata di 64 byte (comprendente i valori r e s). Il server garantisce che la firma sia in formato low-s canonico (EIP-2). Tuttavia, Ethereum richiede un identificatore di recupero (v) per ricavare l'indirizzo del mittente e verificare la firma, che non viene restituito direttamente nelle firme MPC standard a 64 byte.

Lo script Python seguente gestisce questa complessità. Determina dinamicamente il bit di recupero corretto abbinando l'indirizzo del mittente previsto, costruisce la transazione valida e la trasmette alla rete.

Broadcast Script:

Esegui il seguente blocco copiabile e incollabile nel terminale per scrivere ed eseguire 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

Risoluzione dei problemi comuni

  • invalid sender: recovery failed o invalid transaction v, r, s values:questo errore si verifica se i parametri della transazione nello script non corrispondono esattamente al payload firmato. Assicurati che i campi value, nonce e to corrispondano a quanto elaborato nel passaggio 5.
  • insufficient funds for gas * price + value: la transazione è crittograficamente valida ed è stata accettata dal nodo, ma l'indirizzo Ethereum della tua chiave MPC non dispone di ETH testnet sufficienti per coprire l'operazione. Dovrai finanziare il tuo indirizzo prima della trasmissione.

Bitcoin

SegWit Wire Transaction Assembly & Broadcasting

Per le transazioni Bitcoin Native SegWit (BIP 141), la firma MPC (r, s) non elaborata a 64 byte deve essere formattata in ASN.1 DER canonico con S basso, aggiunta con 0x01 (SIGHASH_ALL) e assemblata con la chiave pubblica compressa a 33 byte nello stack di testimoni SegWit a 2 elementi: [DER signature || 0x01, compressed public key].

Broadcast Script:

Esegui il seguente blocco per creare ed eseguire 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

Risoluzione dei problemi comuni

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature):assicurati che il componente s della firma sia in formato low-s canonico (s <= N/2) e codificato correttamente in DER.
  • bad-txns-in-belowout / bad-txns-fee-negative: gli output totali superano il valore di input. Assicurati che il calcolo della variazione (InputSatoshis - PaymentSatoshis - MinerFee) sia strettamente positivo.

📖 Appendice: Operational Cookbook (Querying & Inspecting)

Utilizza questi comandi di sola lettura per ispezionare lo stato delle tue risorse in qualsiasi momento.


👥 1. Partecipanti e firmatari

Ispeziona i partecipanti e i firmatari nella tua istanza.

A. Elenca tutti i partecipanti registrati

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. Visualizza i dettagli di un partecipante specifico

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

Ispeziona le policy e i bundle di attendibilità.

A. Ottieni il bundle di attendibilità dell'autore delle policy

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. Elenca tutte le policy

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. Visualizza i dettagli di una policy specifica

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. Chiavi e keyring

Ispezionare chiavi automatizzate e chiavi.

A. Elenca tutti i keyring

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. Elenca tutte le chiavi in un keyring

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. Ottieni lo stato di una chiave distribuita specifica (stato 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. Proposte (operazioni di firma)

Esaminare le proposte e lo stato della firma.

A. Elenca tutte le proposte per una chiave

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. Visualizza i dettagli o lo stato di una proposta specifica

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