Nutzerhandbuch für Multi-Party Compute

In diesem Leitfaden werden die User Journeys der Multi-Party Computation API zum Verwalten von verteilten Schlüsseln, Nutzern, Teams und Richtlinien beschrieben. Sie richtet sich an Kunden, die die Vorabversion nutzen und die wichtigsten User Journeys kennenlernen möchten.

Übersicht

Die MPC API verwaltet den Lebenszyklus von Instanzen, Teams, Nutzern, Schlüsselbunden, verteilten Schlüsseln und Schwellenwert-Signaturzeremonien. Dabei wird eine integrierte kryptografische Governance über drei Richtlinienebenen hinweg erzwungen:

  • Verwaltungsrichtlinien (Rollen-Quorums): Administratoren binden Nutzer und Teams ein. Ein Quorum von Administratoren (EnableUserRole und ApproveRoleChange) ist erforderlich, um die Rollen Administrator und Richtlinienautor zu gewähren oder zu widerrufen. Die Zuweisung der Rolle Signer wird sofort von einem einzelnen Administrator wirksam, da Signer allein keinen Zugriff auf vorhandene Schlüssel gewährt. Ein Signer erhält nur dann Schlüsselfreigaben, wenn er bei der Schlüsselgenerierung in der KeyPolicy eines KeyRing enthalten ist.
  • Schlüsselrichtlinien (KeyRing.keyPolicy): Administratoren definieren die Schlüsselrichtlinie eines Schlüsselbunds, wenn sie den Schlüsselbund erstellen. Dabei geben sie an, welche Signierer (Signierer, gruppiert in Teams) Schlüsselfreigaben haben und welche K-von-N-Signierer-Genehmigungen (CreateTransaction und RecordSignerDecision) erforderlich sind, um eine Signatur zu autorisieren.
  • Transaktionsrichtlinien (TransactionPolicy und KeyList): Ein Quorum von Richtlinienautoren (POLICY_AUTHOR) schlägt Cedar-permit- und forbid-Regeln (TransactionPolicy) sowie KeyList-Adressen-Zulassungslisten vor, genehmigt sie oder deaktiviert sie. Jede CreateTransaction-Anfrage wird vor dem Signieren anhand aktiver Transaktionsrichtlinien (PERMIT oder DENY) ausgewertet.

Vorbereitung

Prüfen Sie zuerst, ob Sie Folgendes haben:

  1. Ein Google Cloud -Projekt. Informationen zum Erstellen eines Projekts finden Sie unter Projekte erstellen.
  2. Wenn Sie Cloud Shell nicht verwenden, muss die gcloud-Befehlszeile installiert und authentifiziert sein.
  3. Erforderliche APIs aktiviert, falls noch nicht geschehen. Sie benötigen zwei: die in diesem Leitfaden verwendete MPC API und den Cloud Key Management Service, wenn Sie ihn für die Generierung und Signierung von Autorisierungsschlüsseln verwenden.

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

    So rufen Sie die aktivierten APIs auf:

      gcloud services list --enabled
    
  4. Python 3.9 oder höher und die Hilfsbibliotheken für die Anleitung sind installiert:

      pip install embit eth-account eth-utils requests
    
  5. (Optional) Wenn sich Ihre Organisation mit einem externen Identitätsanbieter (z. B. Okta oder Microsoft Entra ID) ohne Google-Konten authentifiziert, finden Sie unter Workforce Identity-Föderation (BYOID) konfigurieren Informationen zum Konfigurieren von Standardanmeldedaten für Anwendungen (Application Default Credentials, ADC) mit Ihrer föderierten Identität.

Einrichtung

Aktualisieren Sie die folgenden Variablen, um die Befehle in diesem Leitfaden zu füllen.

Erforderliche Einrichtungsschritte

Sie müssen diese Parameter anpassen. Die Anleitung funktioniert nicht mit den Platzhalterwerten.

Parameter Wert Beschreibung
Projekt-ID my-project Die Nutzerprojekt-ID für Ihre MPC-Instanz.
Instanz-ID my-mpc-instance Die ID für Ihre MPC-Instanz ([a-z]([a-z0-9-]{0,61}[a-z0-9])?).
Standort us-central1 Bereitstellungsstandort (z. B. us-central1).
Admin 1-Hauptkonto admin-1@example.com Google Cloud -E-Mail-Adresse oder BYOID-Hauptkonto des ersten ADMIN-Nutzers in der Instanz.
Admin 2-Hauptkonto admin-2@example.com Google Cloud -E-Mail-Adresse oder BYOID-Hauptkonto des zweiten ADMIN-Nutzers in der Instanz.
Signer 1 Principal alice@example.com Google Cloud -E-Mail-Adresse oder BYOID-Hauptkonto von Signer 1 (Alice).
Signer 2 Principal bob@example.com Google Cloud E-Mail-Adresse oder BYOID-Hauptkonto von Signer 2 (Bob).
Signer 3 Principal charlie@example.com Google Cloud E-Mail-Adresse oder BYOID-Hauptkonto von Signer 3 (Charlie).
Policy Author 1 Principal dave@example.com Google Cloud -E-Mail-Adresse oder BYOID-Hauptkonto von Policy Author 1 (Dave).
Policy Author 2 Principal eve@example.com Google Cloud -E-Mail-Adresse oder BYOID-Hauptkonto von Policy Author 2 (Eve).

Authentifizieren Sie jedes Konto einmal in Ihrer Terminalsitzung, damit gcloud auth print-access-token "<principal>" Tokens für jede Persona abrufen kann, ohne dass Sie während des Tutorials das Konto wechseln müssen. ADMIN_1_PRINCIPAL wird zuletzt authentifiziert, sodass es das aktive gcloud-CLI-Konto für IAM- und Cloud KMS-Befehle bleibt:

for PRINCIPAL in \
  "admin-2@example.com" \
  "alice@example.com" \
  "bob@example.com" \
  "charlie@example.com" \
  "dave@example.com" \
  "eve@example.com" \
  "admin-1@example.com"; do
  gcloud auth login "$PRINCIPAL"
done

Tutorial-Ressourcenlabels

Dies sind lokale Kennungen für die Autorisierungsschlüssel, die in dieser Anleitung mit Cloud KMS erstellt wurden. Die Standardwerte sind gültig und können beibehalten werden, um den Einrichtungsaufwand zu minimieren. Beachten Sie, dass Ressourcen-IDs für die Ressourcen Team, User, TransactionPolicy, KeyRing, Key, KeyList und Transaction vom Server generiert und in Shell-Variablen (TEAM_NAME, SIGNER_1_NAME, POLICY_NAME, KEY_RING_NAME, KEY_NAME, KEY_LIST_NAME, TRANSACTION_NAME) erfasst werden, wenn die jeweilige Ressource erstellt wird.

Parameter Wert Beschreibung
Cloud KMS-Schlüsselbund-ID mpc-assent-keys Cloud Key Management Service-Schlüsselbund für externe Autorisierungsschlüssel, die in Cloud KMS gespeichert sind.
Cloud KMS Admin 1-ID admin-1 Label für MPC-Administrator 1-Autorisierungsschlüssel.
Cloud KMS Admin 2 ID admin-2 Label für MPC-Admin-2-Autorisierungsschlüssel.
Cloud KMS Signer 1 ID signer-alice Label für den Autorisierungsschlüssel von MPC-Signer 1.
Cloud KMS Signer 2-ID signer-bob Label für den Autorisierungsschlüssel von MPC Signer 2.
Cloud KMS Signer 3-ID signer-charlie Label für den Autorisierungsschlüssel von MPC Signer 3.
Cloud KMS Policy Author 1 ID author-dave Label für den Autorisierungsschlüssel von Richtlinienautor 1.
Cloud KMS Policy Author 2 ID author-eve Label für den Autorisierungsschlüssel von Richtlinienautor 2.

Schritt 1: MPC-Instanz erstellen

Ein MPC-Instance stellt Ihre dedizierte, isolierte MPC-Umgebung dar. Zum Erstellen und Verwalten einer Instanz ist die IAM-Rolle Multi-Party Compute Admin (roles/mpc.admin) für Ihr Projekt erforderlich. Sie erstellen und prüfen die Instance als Administrator mit MPCInstanceService auf dem Endpunkt der Steuerungsebene (https://mpc.googleapis.com) und senden alle nachfolgenden Ressourcenanfragen an die dedizierte URL der Datenebene der Instanz (INSTANCE_URL).

A. Instanz erstellen

Prüfen Sie zuerst, ob Ihre Administrator-Hauptkonten (admin-1@example.com und admin-2@example.com) die Berechtigung roles/mpc.admin für das Projekt haben, und rufen Sie instances.create (CreateInstance) als Administrator 1 auf, um Ihre MPC-Instanz bereitzustellen:

curl

grant_iam_role() {
  local principal=$1
  local iam_role=$2
  local member="user:${principal}"

  # BYOID principals already start with "principal://"
  if [[ "$principal" == principal://* ]]; then
    member="${principal}"
  fi

  echo "Granting ${iam_role} to ${member}..."
  gcloud projects add-iam-policy-binding "my-project" \
    --member="${member}" \
    --role="${iam_role}" \
    --condition=None > /dev/null
}

for ADMIN_PRINCIPAL in "admin-1@example.com" "admin-2@example.com"; do
  grant_iam_role "$ADMIN_PRINCIPAL" "roles/mpc.admin"
done

export INSTANCE_NAME="projects/my-project/locations/us-central1/instances/my-mpc-instance"

# Check existence
if curl -s -f -o /dev/null -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}"; then
  echo "Instance 'my-mpc-instance' already exists. Skipping creation."
else
  echo "Instance 'my-mpc-instance' not found. Creating..."
  curl -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d '{
      "displayName": "Tutorial MPC Instance"
    }' \
    "https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances?instanceId=my-mpc-instance" | jq
fi

Dadurch wird ein Vorgang mit langer Ausführungszeit (Long-Running Operation, LRO) zurückgegeben. Sie müssen warten, bis die Instanz state den Status ACTIVE hat, bevor Sie die URL der Datenebene aufrufen.

🕵️‍♂️ Instanzstatus prüfen (Polling)

Fragen Sie den Instanzstatus ab, bis state zu ACTIVE wird, und speichern Sie das Feld url in INSTANCE_URL:

curl

INSTANCE_INFO=$(curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}")

echo "$INSTANCE_INFO" | jq
export INSTANCE_URL=$(echo "$INSTANCE_INFO" | jq -r '.url')
echo "Instance Data Plane URL: $INSTANCE_URL"

Schritt 2: Autorisierungsschlüssel generieren

Wenn Sie die MPC API verwenden möchten, müssen Sie Schlüsselpaare für die folgenden Rollen generieren. Beachten Sie, dass es sich hierbei um externe kryptografische Autorisierungsschlüssel (User.authorizationKey) handelt. Das bedeutet, dass Sie sie außerhalb der MPC-Instanz generieren und verwalten. Sie unterscheiden sich daher von den MPC-Schlüsselfragmenten, die während der Schlüsselerstellung generiert werden:

  • Administratoren: Können Änderungen an den Nutzerrollen ADMIN und POLICY_AUTHOR autorisieren und Keys erstellen.
  • Richtlinienautoren: Governance-Richtlinien autorisieren (TransactionPolicy).
  • Unterzeichner: Erteilen die Autorisierung (AUTHORIZE oder DENY) für Transaktionen.

Generieren Sie die kryptografischen secp256k1-Autorisierungsschlüssel für Ihre Signierer, Richtlinienautoren und Administratoren. Diese Befehle sind idempotent.

Cloud KMS

Wir empfehlen, Autorisierungsschlüssel im Cloud Key Management Service zu verwalten. Ihre Identität muss die Berechtigungen zum Erstellen von Schlüsselbunden, Erstellen von Schlüsseln, Signieren und Aufrufen von Prüfern für öffentliche Schlüssel (z. B. roles/cloudkms.admin und roles/cloudkms.signerVerifier) haben. Sie müssen einen KeyRing erstellt haben. Generieren Sie dann einen secp256k1-Schlüssel für jeden Signierer, Richtlinienautor und Administrator.

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 Signers, Policy Authors, and Admins
for ID in \
  signer-alice \
  signer-bob \
  signer-charlie \
  author-dave \
  author-eve \
  admin-1 \
  admin-2; do
  echo "Generating KMS key for $ID..."
  provision_kms_key "$ID"
done

OpenSSL

Sie können Standard-OpenSSL verwenden, um secp256k1-Schlüssel für jeden Signierer, Richtlinienautor und Administrator zu generieren.

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 \
  author-dave \
  author-eve \
  admin-1 \
  admin-2; do
  echo "Generating OpenSSL key for $ID..."
  provision_openssl_key "$ID"
done

Schritt 3: Teams und Nutzer registrieren

Bevor eine Entität an der Transaktionssignierung (als SIGNER) oder der Richtlinienerstellung (als POLICY_AUTHOR) teilnehmen kann, müssen ihr IAM-Berechtigungen für das Projekt gewährt werden und sie muss als User auf Anwendungsebene in der Instanz registriert sein (siehe Sicherheit und Autorisierung für Informationen dazu, wie IAM-Rollen und User-Rollen zusammenarbeiten).

Für die Registrierung ist eine Out-of-Band-Koordination zwischen dem Administrator und dem Nutzer erforderlich:

  1. Die Entität (Signierer oder Richtlinienautor) generiert ihr Schlüsselpaar lokal und überträgt ihren öffentlichen Schlüssel sicher an den Administrator.
  2. Der Administrator weist dem Hauptkonto des Nutzers die entsprechende IAM-Rolle für das Projekt zu (roles/mpc.editor für Signierer und Richtlinienautoren; roles/mpc.admin für Administratoren) und ruft die MPC API auf, um die principal und authorizationKey des Nutzers zu registrieren.

A. Signer-Team erstellen

Jeder User mit der Rolle SIGNER muss zu einem Team gehören. Alle anderen Users mit anderen Rollen dürfen nicht zu einem Team gehören. Erstellen Sie als Admin 1 (admin-1@example.com) eine Team (CreateTeam) mit dem Namen „Treasury Signers“, um Ihre drei Signierer zu speichern, und erfassen Sie die vom Server generierte Ressource name:

curl

TEAM_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Treasury Signers",
  }' \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams?requestId=11111111-1111-4111-8111-111111111101")

echo "$TEAM_RESPONSE" | jq
export TEAM_NAME=$(echo "$TEAM_RESPONSE" | jq -r '.name')
echo "Created Team: $TEAM_NAME"

B. IAM-Rollen zuweisen und Signierer und Richtlinienautoren registrieren

Jeder Nutzer benötigt sowohl eine IAM-Rolle auf Projektebene (roles/mpc.editor für Signierer und Richtlinienautoren und roles/mpc.admin für Administratoren), um die MPC API aufzurufen, als auch eine User-Ressource auf Anwendungsebene innerhalb der Instanz. Beim Aufrufen von users.create (CreateUser) kann das Feld roles nur leer oder ["SIGNER"] sein:

  • Signierer: In IAM wird ihnen roles/mpc.editor gewährt und sie werden mit roles: ["SIGNER"] und team: "$TEAM_NAME" erstellt. Die Rolle SIGNER wird sofort zu ACTIVE, ohne dass die Genehmigung eines Administrator-Quorums erforderlich ist, da die Rolle SIGNER allein keinen Zugriff auf vorhandene Schlüssel gewährt. Ein Signer erhält nur dann Schlüsselfreigaben, wenn er bei der Schlüsselgenerierung in der KeyPolicy eines KeyRing enthalten ist.
  • Richtlinienautoren: Ihnen wird roles/mpc.editor in IAM gewährt und sie werden ohne anfängliche Rollen und ohne team erstellt. Anschließend wird ihnen in Schritt 3.C die Rolle POLICY_AUTHOR mit der Genehmigung des Administrator-Quorums gewährt (da POLICY_AUTHOR und ADMIN instanzweite Governance-Befugnisse haben).

curl

provision_user() {
  local id=$1
  local display_name=$2
  local principal=$3
  local request_id=$4
  local team=$5
  local pubkey_b64
  local signer_fields=""

  # 1. Grant project-level IAM permission (roles/mpc.editor) to the User's principal
  grant_iam_role "$principal" "roles/mpc.editor" >&2

  # 2. Read the public key from the file generated in Step 2 (Base64-encoded PEM/DER bytes)
  pubkey_b64=$(openssl base64 -A -in "${id}_pub.pem")

  if [[ -n "$team" ]]; then
    signer_fields=", \"roles\": [\"SIGNER\"], \"team\": \"${team}\""
  fi

  # 3. Register the application-level User in the MPC instance as Admin 1
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"displayName\": \"${display_name}\",
      \"principal\": \"${principal}\",
      \"authorizationKey\": {
        \"publicKey\": \"${pubkey_b64}\",
        \"algorithm\": \"PUBLIC_KEY_SIGNATURE_ALGORITHM_ECDSA_SECP256K1\"
      }${signer_fields}
    }" \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users?requestId=${request_id}"
}

echo "Registering Signers..."
export SIGNER_1_NAME=$(provision_user "signer-alice" "Signer Alice" "alice@example.com" "11111111-1111-4111-8111-111111111001" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_2_NAME=$(provision_user "signer-bob" "Signer Bob" "bob@example.com" "11111111-1111-4111-8111-111111111002" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')
export SIGNER_3_NAME=$(provision_user "signer-charlie" "Signer Charlie" "charlie@example.com" "11111111-1111-4111-8111-111111111003" "${TEAM_NAME}" | tee /dev/stderr | jq -r '.name')

echo "Registering Policy Authors..."
export POLICY_AUTHOR_1_NAME=$(provision_user "author-dave" "Author Dave" "dave@example.com" "11111111-1111-4111-8111-111111111004" | tee /dev/stderr | jq -r '.name')
export POLICY_AUTHOR_2_NAME=$(provision_user "author-eve" "Author Eve" "eve@example.com" "11111111-1111-4111-8111-111111111005" | tee /dev/stderr | jq -r '.name')

C. Rolle „Policy Author“ (Genehmigung durch Administrator-Quorum) zuweisen

Zum Aktivieren oder Deaktivieren der Rolle POLICY_AUTHOR oder ADMIN für ein User ist ein Quorum von Administratoren erforderlich:

  1. Admin 1 ruft users.enableRole (EnableUserRole) auf, wodurch die Rolle zu PENDING_ACTIVE verschoben und die Signatur von Admin 1 als erste Genehmigung aufgezeichnet wird.
  2. Admin 2 ruft users.approveRoleChange (ApproveRoleChange) mit seiner Signatur auf und überträgt die Rolle an ACTIVE.

Cloud KMS

1. POLICY_AUTHOR als Admin 1 (EnableUserRole) anfordern

sign_role_change() {
  local admin_id=$1
  local target_id=$2
  local principal=$3
  python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${admin_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=role_payload.bin \
    --signature-file=role_sig.bin > /dev/null
  openssl base64 -A -in role_sig.bin
}

enable_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
  local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"role\": \"POLICY_AUTHOR\",
      \"authorizationInfo\": {
        \"signature\": \"${admin_1_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}

enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

2. POLICY_AUTHOR als Admin 2 (ApproveRoleChange) genehmigen

approve_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
  local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${admin_2_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}

approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

OpenSSL

1. POLICY_AUTHOR als Admin 1 (EnableUserRole) anfordern

sign_role_change() {
  local admin_id=$1
  local target_id=$2
  local principal=$3
  python3 -c 'import sys; inst, prin, pub = sys.argv[1], sys.argv[2], open(sys.argv[3], "rb").read(); v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("role_payload.bin", "wb").write(f(1, inst.encode()) + f(2, prin.encode()) + f(3, f(1, pub) + b"\x10\x01") + f(4, b"\x08\x02\x10\x01\x18\x01"))' "${INSTANCE_NAME}" "${principal}" "${target_id}_pub.pem"
  openssl dgst -sha256 -sign "${admin_id}_priv.pem" -out role_sig.bin role_payload.bin
  openssl base64 -A -in role_sig.bin
}

enable_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 1 requesting POLICY_AUTHOR for ${target_user}..."
  local admin_1_sig=$(sign_role_change "admin-1" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"role\": \"POLICY_AUTHOR\",
      \"authorizationInfo\": {
        \"signature\": \"${admin_1_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:enableRole" | jq
}

enable_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
enable_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

2. POLICY_AUTHOR als Admin 2 (ApproveRoleChange) genehmigen

approve_policy_author() {
  local target_user=$1
  local target_id=$2
  local target_principal=$3

  echo "Admin 2 approving POLICY_AUTHOR for ${target_user}..."
  local admin_2_sig=$(sign_role_change "admin-2" "${target_id}" "${target_principal}")
  local signed_payload_b64=$(openssl base64 -A -in role_payload.bin)

  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-2@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${admin_2_sig}\",
        \"signedPayload\": \"${signed_payload_b64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${target_user}:approveRoleChange" | jq
}

approve_policy_author "${POLICY_AUTHOR_1_NAME}" "author-dave" "dave@example.com"
approve_policy_author "${POLICY_AUTHOR_2_NAME}" "author-eve" "eve@example.com"

Schritt 4: Transaktionsrichtlinien konfigurieren

Konfigurieren Sie Transaktionsrichtlinien, indem Sie eine TransactionPolicy (und optional eine KeyList) erstellen und genehmigen.

A. Richtlinie erstellen (PENDING_ACTIVE)

Erstellen Sie eine Cedar-Richtlinienregel, die beschreibt, welche Transaktionen zulässig sind. Sie können viele Richtlinien für verschiedene Szenarien konfigurieren. In diesem Leitfaden wird jedoch die Einrichtung einer Richtlinie für Ethereum- oder Bitcoin-Transaktionen beschrieben. Standardmäßig ist die Auswertung der Transaktionsrichtlinie Fail-Fast (Block by Default). Wenn keine aktive permit-Richtlinie mit einer Transaktion übereinstimmt, lautet die Entscheidung der Richtlinienauswertung DENY. Ausführliche Informationen zu Cedar-Attributen finden Sie unter Regeln für Transaktionsrichtlinien schreiben.

Ethereum, KMS

Diese Regel erlaubt kleinere Ethereum-Überweisungen (<= 2 ETH, dargestellt als 2,000,000,000 in Einheiten von 10-9 ETH / Gwei) an eine bestimmte Empfängeradresse (0x1111111111111111111111111111111111111111).

permit (principal, action, resource)
when {
  context.schema == "ETHEREUM_TX_V2" &&
  context.secondary_key == "0x1111111111111111111111111111111111111111" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2000000000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=policy_payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Ethereum Transfer Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111201"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Ethereum, OpenSSL

Diese Regel erlaubt kleinere Ethereum-Überweisungen (<= 2 ETH, dargestellt als 2,000,000,000 in Einheiten von 10-9 ETH / Gwei) an eine bestimmte Empfängeradresse (0x1111111111111111111111111111111111111111).

permit (principal, action, resource)
when {
  context.schema == "ETHEREUM_TX_V2" &&
  context.secondary_key == "0x1111111111111111111111111111111111111111" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2000000000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "ETHEREUM_TX_V2" && context.secondary_key == "0x1111111111111111111111111111111111111111" && context has amount_token_scaled && context.amount_token_scaled <= 2000000000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Ethereum Transfer Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111201"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin, KMS

Diese Regel erlaubt Bitcoin-Überweisungen mit dem Schema BITCOIN_TX_V2 (BIP 174 PSBT v0), bei denen der Netto-Überweisungsbetrag 250.000 Satoshis (context.amount_token_scaled <= 2500000 oder 0, 0025 BTC,normalisiert auf 10-9 feste Dezimaleinheiten) und die Miner-Gebühr 10.000 Satoshis (context.max_fee_atomic <= 100000) nicht überschreitet.

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${author_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=policy_payload.bin \
    --signature-file=policy_sig.bin > /dev/null
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

Bitcoin, OpenSSL

Diese Regel erlaubt Bitcoin-Überweisungen mit dem Schema BITCOIN_TX_V2 (BIP 174 PSBT v0), bei denen der Netto-Überweisungsbetrag 250.000 Satoshis (context.amount_token_scaled <= 2500000 oder 0, 0025 BTC,normalisiert auf 10-9 feste Dezimaleinheiten) und die Miner-Gebühr 10.000 Satoshis (context.max_fee_atomic <= 100000) nicht überschreitet.

permit (principal, action, resource)
when {
  context.schema == "BITCOIN_TX_V2" &&
  context has amount_token_scaled &&
  context.amount_token_scaled <= 2500000 &&
  context.max_fee_atomic <= 100000
};
sign_policy() {
  local author_id=$1
  local resource_type=$2
  local resource_name=$3
  local action=$4
  local cedar_doc=$5
  python3 -c 'import sys; rt, rn, act, doc = sys.argv[1:]; v = lambda n: bytes([n & 127 | 128]) + v(n >> 7) if n > 127 else bytes([n]); f = lambda k, b: bytes([k << 3 | 2]) + v(len(b)) + b; open("policy_payload.bin", "wb").write(f(1 if rt == "instance" else 2, rn.encode()) + bytes([24, 1 if act == "ACTIVATE" else 2]) + f(4, f(1, doc.encode())))' "${resource_type}" "${resource_name}" "${action}" "${cedar_doc}"
  openssl dgst -sha256 -sign "${author_id}_priv.pem" -out policy_sig.bin policy_payload.bin
  openssl base64 -A -in policy_sig.bin
}

export CEDAR_RULE='permit (principal, action, resource) when { context.schema == "BITCOIN_TX_V2" && context has amount_token_scaled && context.amount_token_scaled <= 2500000 && context.max_fee_atomic <= 100000 };'

# Sign as Policy Author 1
QUORUM_SIGNATURE=$(sign_policy "author-dave" "instance" "${INSTANCE_NAME}" "ACTIVATE" "${CEDAR_RULE}")
POLICY_PAYLOAD_B64=$(openssl base64 -A -in policy_payload.bin)

POLICY_RESPONSE=$(jq -n \
  --arg rule "$CEDAR_RULE" \
  --arg sig "$QUORUM_SIGNATURE" \
  --arg payload "$POLICY_PAYLOAD_B64" \
  '{
    transactionPolicy: {
      displayName: "Bitcoin PSBT Permit Policy",
      rule: {cedarDocument: $rule}
    },
    authorizationInfo: {
      signature: $sig,
      signedPayload: $payload
    },
    requestId: "11111111-1111-4111-8111-111111111202"
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "dave@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies")

echo "$POLICY_RESPONSE" | jq
export POLICY_NAME=$(echo "$POLICY_RESPONSE" | jq -r '.name')

B. Richtlinie genehmigen (Übergang zu AKTIV)

Richtlinienänderungen erfordern 2-von-2-Signaturen. Daher muss ein zweiter Richtlinienautor transactionPolicies.approve (ApproveTransactionPolicy) mit einer signierten TransactionPolicySigningPayload aufrufen.

Ethereum

# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}")

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

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  # 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
  QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
  POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)

  # 3. Call ApproveTransactionPolicy as Policy Author 2
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${QUORUM_SIGNATURE_2}\",
        \"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
  echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi

Bitcoin

# 1. Check the policy state
POLICY_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}")

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

if [ "$POLICY_STATE" == "PENDING_ACTIVE" ]; then
  # 2. Sign as Policy Author 2 naming transaction_policy ($POLICY_NAME)
  QUORUM_SIGNATURE_2=$(sign_policy "author-eve" "transaction_policy" "${POLICY_NAME}" "ACTIVATE" "${CEDAR_RULE}")
  POLICY_PAYLOAD_2_B64=$(openssl base64 -A -in policy_payload.bin)

  # 3. Call ApproveTransactionPolicy as Policy Author 2
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "eve@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"authorizationInfo\": {
        \"signature\": \"${QUORUM_SIGNATURE_2}\",
        \"signedPayload\": \"${POLICY_PAYLOAD_2_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${POLICY_NAME}:approve" | jq
elif [ "$POLICY_STATE" == "ACTIVE" ]; then
  echo "Policy '${POLICY_NAME}' is already ACTIVE. Skipping approval."
else
  echo "Policy '${POLICY_NAME}' is in an unexpected state: $POLICY_STATE"
fi

C. KeyList erstellen und verwalten (optional)

Ein KeyList (KeyListService) ist eine benannte Liste von Blockchain-Adressen (manualEntry, MPC-key oder keyRing), auf die Richtlinien als vertrauenswürdige Ziele verweisen können. Administratoren (ADMIN) können mit keyLists.create eine KeyList erstellen und die Einträge jederzeit mit keyLists.addEntries und keyLists.removeEntries aktualisieren:

curl

KEY_LIST_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Approved Destinations",
    "description": "Trusted recipient addresses",
    "entries": [
      {
        "manualEntry": {
          "address": "0x1111111111111111111111111111111111111111",
          "source": "Treasury onboarding",
          "reason": "Approved recipient address"
        }
      }
    ]
  }' \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists?requestId=11111111-1111-4111-8111-111111111301")

echo "$KEY_LIST_RESPONSE" | jq
export KEY_LIST_NAME=$(echo "$KEY_LIST_RESPONSE" | jq -r '.name')

Schritt 5: Schlüssel erstellen

Erstellen Sie als Administrator 1 (admin-1@example.com) einen KeyRing mit einem Inline-KeyPolicy und generieren Sie einen verteilten MPC-Schlüssel.

A. Schlüsselbund mit einer Inline-Schlüsselrichtlinie erstellen

Ein KeyRing ist eine logische Gruppierung für Ihre MPC-Schlüssel und definiert eine unveränderliche Inline-KeyPolicy (keyRing.keyPolicy), der jeder im KeyRing erstellte Schlüssel folgt. Ein KeyPolicy ist eine Baumstruktur von Schwellenwertregeln (PolicyNode), deren untergeordnetes subKeyShares ein teamNode (threshold von totalMembers in einem Team), ein userNode (ein bestimmter SIGNER User) oder ein verschachteltes policyNode sein kann.

Hier konfigurieren wir eine 2-von-3-Schwellenwertregel für die drei Signierer in ${TEAM_NAME} (threshold: 2, totalMembers: 3, gespeichert in GCP_TEE):

curl

KEY_RING_RESPONSE=$(jq -n \
  --arg team "${TEAM_NAME}" \
  '{
    displayName: "Treasury KeyRing",
    keyPolicy: {
      policy: {
        policyNodeId: "treasury-2-of-3",
        threshold: 1,
        subKeyShares: [
          {
            teamNode: {
              team: $team,
              threshold: 2,
              totalMembers: 3,
              storageInfo: {storageType: "GCP_TEE"}
            }
          }
        ]
      }
    }
  }' \
| curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d @- \
    "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings?requestId=11111111-1111-4111-8111-111111111401")

echo "$KEY_RING_RESPONSE" | jq
export KEY_RING_NAME=$(echo "$KEY_RING_RESPONSE" | jq -r '.name')
echo "Created KeyRing: $KEY_RING_NAME"

B. Verteilten Schlüssel generieren (DKG)

Rufen Sie keys.create (CreateKey) als Admin 1 auf, um die verteilte Schlüsselgenerierung (Distributed Key Generation, DKG) für die Teilnehmer auszuführen, die von der KeyPolicy Ihrer KeyRing ausgewählt wurden.

CreateKey ist synchron und gibt die ACTIVE-Ressource Key zurück, einschließlich publicKeyInfo.publicKey und vom Server abgeleiteter publicKeyInfo.blockchainAddresses (EC_SIGN_SECP256K1 für Ethereum und Bitcoin oder EC_SIGN_ED25519 für Solana, sobald es unterstützt wird):

curl

KEY_RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d '{
    "algorithm": "EC_SIGN_SECP256K1"
  }' \
  "${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys?requestId=11111111-1111-4111-8111-111111111501")

echo "$KEY_RESPONSE" | jq
export KEY_NAME=$(echo "$KEY_RESPONSE" | jq -r '.name')
export PUBLIC_KEY_B64=$(echo "$KEY_RESPONSE" | jq -r '.publicKeyInfo.publicKey')

C. Blockchain-Adresse ableiten und UTXOs finden

Wenn CreateKey abgeschlossen ist, enthält publicKeyInfo.blockchainAddresses in der Antwort automatisch abgeleitete Adressen (secp256k1Addresses.ethereumAddress und secp256k1Addresses.bitcoinMainnetAddresses für EC_SIGN_SECP256K1 oder ed25519Addresses.solanaAddress für EC_SIGN_ED25519). Sie können Testnetz-Adressen und Sperrskripts auch unabhängig voneinander mit den folgenden Skripts aus dem unkomprimierten 65-Byte-öffentlichen Schlüssel (0x04 || X || Y) verifizieren oder ableiten und alle nicht ausgegebenen Mittel finden, die für Transaktionen erforderlich sind.

Ethereum

Ethereum-Adressen werden in .publicKeyInfo.blockchainAddresses.secp256k1Addresses.ethereumAddress zurückgegeben und können auch aus dem Keccak-256-Hash des unkomprimierten öffentlichen Schlüsselpunkts mit 64 Byte (pub_bytes[1:]) abgeleitet werden, wobei die letzten 20 Byte verwendet werden.

Address Derivation Script (derive_address.py)

Führen Sie den folgenden Block aus, um derive_address.py zu erstellen und auszuführen:

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

# Insert your base64 publicKey from Step 5.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

Ethereum-Adresse mit Guthaben aufladen

Bevor Sie die Transaktion in Schritt 7 übertragen, müssen Sie sicherstellen, dass Ihre abgeleitete Ethereum-Adresse genügend Sepolia-Testnetz-ETH hat, um den Transaktionswert und die Gaskosten zu decken. Sie können kostenlose Testnetz-ETH anfordern, indem Sie Ihre neu abgeleitete Adresse in das Faucet für Ethereum Sepolia eingeben.

Bitcoin

Bitcoin-Mainnet-Adressen (p2wpkhAddress, p2shP2wpkhAddress und p2pkhAddress) werden in .publicKeyInfo.blockchainAddresses.secp256k1Addresses.bitcoinMainnetAddresses zurückgegeben. Aus dem unkomprimierten 65-Byte-EC-öffentlichen Schlüssel (0x04 || X || Y) von secp256k1 können Sie auch den komprimierten 33-Byte-öffentlichen Schlüssel, Sperrskripts und Testnet4-Adressen in allen drei Formaten ableiten:

  1. Native SegWit (P2WPKH / BIP 173): Bech32-Adresse (bc1q... im Mainnet, tb1q... im Testnet4) mit einem 22-Byte-Witness-scriptPubKey (0014<hash160>). Dies ist das empfohlene moderne Format mit den niedrigsten Transaktionsgebühren.
  2. Nested SegWit (P2SH-P2WPKH / BIP 49): Wrapped SegWit-Adresse (3... im Mainnet, 2... im Testnet4) mit einem 23-Byte-scriptPubKey (a914<hash160>87) und einem 22-Byte-RedeemScript (0014<hash160>), kompatibel mit Legacy-Systemen.
  3. Legacy (P2PKH / BIP 44): Originale Pay-to-PubKey-Hash-Base58-Adresse (1... im Mainnet, m/n... im Testnet4) mit einem 25-Byte-scriptPubKey (76a914<hash160>88ac).

Address Derivation Script (derive_btc_address.py)

Führen Sie den folgenden Block aus, um derive_btc_address.py zu erstellen und auszuführen:

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 5.B (publicKeyInfo.publicKey, 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

Nicht ausgegebene Ausgaben (UTXOs) finden

Im Gegensatz zu kontobasierten Blockchains werden bei Bitcoin-Transaktionen bestimmte nicht ausgegebene Transaktionsausgaben (Unspent Transaction Outputs, UTXOs) ausgegeben. Bevor Sie in Schritt 6 eine Transaktion erstellen, müssen Sie Folgendes tun:

  1. Zahlen Sie Guthaben auf Ihre abgeleitete Native SegWit-Adresse ein (z. B. über ein Testnetz-Faucet).
  2. Fragen Sie die nicht ausgegebenen UTXOs Ihrer Adresse mit Ihrem Bitcoin-Knoten (bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") oder einer Block-Explorer-API ab.
  3. Beachten Sie die folgenden vier Parameter für Schritt 6:
    • txid: Der 32-Byte-Transaktions-Hash der nicht ausgegebenen Ausgabe.
    • vout: Der nullbasierte Ausgabeindex (z. B. 0 oder 1).
    • value: Der nicht ausgegebene Betrag in Satoshis (z. B. 200000 Satoshis = 0, 002 BTC).
    • scriptPubKey: Der Hexadezimalwert des Sperrskripts (z. B. 0014...).

Schritt 6: MPC-Vorgang ausführen

Die Richtlinienbewertung und die Initiierung der Signatur erfolgen zusammen, wenn ein SIGNER transactions.create (CreateTransaction) aufruft. Die Instanz decodiert payload gemäß schema, wertet alle aktiven Transaktionsrichtlinien aus, zeichnet das Ergebnis in policyEvaluationRecord auf und zeichnet die Entscheidung des initiierenden Signierers AUTHORIZE auf.

A. Transaktion erstellen (Richtlinie bewerten und Signieren initiieren)

Senden Sie die Transaktion als Signer 1 (alice@example.com) zur Richtlinienbewertung und Signierung:

Ethereum, KMS

🛑 Fall A: Ablehnung (zu hoher Wert)

Versuchen Sie, eine Transaktion von 3 ETH zu erstellen (überschreitet das Richtlinienlimit von 2 ETH):

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${signer_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=tx_decision_payload.bin \
    --signature-file=tx_decision_sig.bin > /dev/null
  openssl base64 -A -in tx_decision_sig.bin
}

# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111601\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

Erwartetes Ergebnis der Richtlinienbewertung: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Fall B: Genehmigung (zulässiger Wert)

Erstellen Sie eine Transaktion von 1 Gwei (innerhalb unseres Limits von 2 ETH):

# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111602\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

Erwartete Antwort: state: PENDING_AUTHORIZATION und policyEvaluationRecord.decision: PERMIT.

Ethereum, OpenSSL

🛑 Fall A: Ablehnung (zu hoher Wert)

Versuchen Sie, eine Transaktion von 3 ETH zu erstellen (überschreitet das Richtlinienlimit von 2 ETH):

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
  openssl base64 -A -in tx_decision_sig.bin
}

# Payload for 3 ETH (amount_token_scaled = 3,000,000,000 > 2,000,000,000)
RAW_TX_BYTES_HEX="ef808504a817c8008252089411111111111111111111111111111111111111118829a2241af62c00008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111601\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

Erwartetes Ergebnis der Richtlinienbewertung: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Fall B: Genehmigung (zulässiger Wert)

Erstellen Sie eine Transaktion von 1 Gwei (innerhalb unseres Limits von 2 ETH):

# Payload for 1 Gwei
RAW_TX_BYTES_HEX="eb808504a817c800825208941111111111111111111111111111111111111111843b9aca008083aa36a78080"
PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$RAW_TX_BYTES_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from eth_utils import keccak; print(keccak(bytes.fromhex('$RAW_TX_BYTES_HEX')).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"ETHEREUM_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111602\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

Erwartete Antwort: state: PENDING_AUTHORIZATION und policyEvaluationRecord.decision: PERMIT.

Bitcoin, KMS

Im Bitcoin-UTXO-Modell werden bei Transaktionen bestimmte nicht ausgegebene Ausgaben ausgegeben. In SegWit (BIP 143) müssen für die Berechnung des Signatur-Hashs der Satoshi-Betrag und das Sperrskript des ausgegebenen Inputs bekannt sein.

Für die Bitcoin-Richtlinienbewertung ist das BIP 174 PSBT v0-Format (BITCOIN_TX_V2) erforderlich, wobei PSBT_IN_WITNESS_UTXO für jede Eingabe ausgefüllt sein muss.

PSBT-Konstruktionsskript (build_psbt.py)

Führen Sie den folgenden Block aus, um eine gültige BIP 174 PSBT v0-Nutzlast zusammenzustellen:

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 5.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"

🛑 Fall A: Ablehnung (Gebührenlimit überschritten)

Versuchen Sie, eine Transaktion zu erstellen, bei der die Miner-Gebühr das Richtlinienlimit von 10.000 Satoshi überschreitet. In diesem Beispiel wird durch das Festlegen von MINER_FEE_SATS = 15000 (und CHANGE_SATS = 135000) in build_psbt.py auf max_fee_atomic = 150000 skaliert (was die Richtlinienobergrenze von 100000 überschreitet). Dadurch wird die folgende Nutzlast erzeugt, die eine Richtlinienbewertungsentscheidung vom Typ DENY auslöst.

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  gcloud kms asymmetric-sign \
    --version=1 \
    --key="${signer_id}-key" \
    --keyring="mpc-assent-keys" \
    --location="us-central1" \
    --digest-algorithm=sha256 \
    --input-file=tx_decision_payload.bin \
    --signature-file=tx_decision_sig.bin > /dev/null
  openssl base64 -A -in tx_decision_sig.bin
}

# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${REJECTED_PAYLOAD_B64}\",
      \"digest\": \"${REJECTED_DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111603\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

Erwartetes Ergebnis der Richtlinienbewertung: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Fall B: Genehmigung (zulässige Bitcoin-PSBT)

Erstellen Sie eine Transaktion mit der zuvor erstellten gültigen PSBT (RECIPIENT_SATS = 50000 wird amount_token_scaled = 500000 <= 2500000 zugeordnet und MINER_FEE_SATS = 250 wird max_fee_atomic = 2500 <= 100000 zugeordnet):

PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111604\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

Erwartete Antwort: state: PENDING_AUTHORIZATION und policyEvaluationRecord.decision: PERMIT.

Bitcoin, OpenSSL

Im Bitcoin-UTXO-Modell werden bei Transaktionen bestimmte nicht ausgegebene Ausgaben ausgegeben. In SegWit (BIP 143) müssen für die Berechnung des Signatur-Hashs der Satoshi-Betrag und das Sperrskript des ausgegebenen Inputs bekannt sein.

Für die Bitcoin-Richtlinienbewertung ist das BIP 174 PSBT v0-Format (BITCOIN_TX_V2) erforderlich, wobei PSBT_IN_WITNESS_UTXO für jede Eingabe ausgefüllt sein muss.

PSBT-Konstruktionsskript (build_psbt.py)

Führen Sie den folgenden Block aus, um eine gültige BIP 174 PSBT v0-Nutzlast zusammenzustellen:

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 5.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"

🛑 Fall A: Ablehnung (Gebührenlimit überschritten)

Versuchen Sie, eine Transaktion zu erstellen, bei der die Miner-Gebühr das Richtlinienlimit von 10.000 Satoshi überschreitet. In diesem Beispiel wird durch das Festlegen von MINER_FEE_SATS = 15000 (und CHANGE_SATS = 135000) in build_psbt.py auf max_fee_atomic = 150000 skaliert (was die Richtlinienobergrenze von 100000 überschreitet). Dadurch wird die folgende Nutzlast erzeugt, die eine Richtlinienbewertungsentscheidung vom Typ DENY auslöst.

sign_tx_decision() {
  local signer_id=$1
  local resource_type=$2
  local resource_name=$3
  local decision=$4
  local digest_hex=$5
  python3 -c 'import sys; rt, rn, dec, dg = sys.argv[1:]; f = lambda k, b: bytes([k << 3 | 2, len(b)]) + b; open("tx_decision_payload.bin", "wb").write(f(1 if rt == "key" else 2, rn.encode()) + bytes([24, 1 if dec == "AUTHORIZE" else 2]) + f(4, bytes.fromhex(dg)))' "${resource_type}" "${resource_name}" "${decision}" "${digest_hex}"
  openssl dgst -sha256 -sign "${signer_id}_priv.pem" -out tx_decision_sig.bin tx_decision_payload.bin
  openssl base64 -A -in tx_decision_sig.bin
}

# Construct a PSBT payload with miner_fee_sats = 15000 -> max_fee_atomic = 150000 (exceeds 100000 limit)
REJECTED_PSBT_HEX="70736274ff0100710200000001d81639a6635e3921442629bc7f51f22ca9e4ca3d27c280c9f1936915334f598f0100000000ffffffff0250c3000000000000160014751e76e8199196d454941c45d1b3a323f1433bd6580f020000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000000001011f400d030000000000160014eced05cd51d964102a20c856b09080ad620e9da2000000"
REJECTED_PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_PSBT_HEX')).decode('utf-8'))")
REJECTED_DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$REJECTED_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
REJECTED_DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$REJECTED_DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${REJECTED_DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${REJECTED_PAYLOAD_B64}\",
      \"digest\": \"${REJECTED_DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111603\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

Erwartetes Ergebnis der Richtlinienbewertung: decision: DENY (denyReason: NO_PERMIT_RULE_MATCHED).

✅ Fall B: Genehmigung (zulässige Bitcoin-PSBT)

Erstellen Sie eine Transaktion mit der zuvor erstellten gültigen PSBT (RECIPIENT_SATS = 50000 wird amount_token_scaled = 500000 <= 2500000 zugeordnet und MINER_FEE_SATS = 250 wird max_fee_atomic = 2500 <= 100000 zugeordnet):

PAYLOAD_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$BTC_PSBT_HEX')).decode('utf-8'))")
export DIGEST_HEX=$(python3 -c "from embit import script; from embit.psbt import PSBT; p=PSBT.parse(bytes.fromhex('$BTC_PSBT_HEX')); u=p.inputs[0].witness_utxo; sc=script.Script(b'\x76\xa9\x14'+u.script_pubkey.data[2:]+b'\x88\xac'); print(p.tx.sighash_segwit(0, sc, u.value).hex())")
export DIGEST_B64=$(python3 -c "import base64; print(base64.b64encode(bytes.fromhex('$DIGEST_HEX')).decode('utf-8'))")

SIGNER_1_SIG_B64=$(sign_tx_decision "signer-alice" "key" "${KEY_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
SIGNED_DECISION_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

RESPONSE=$(curl -s -X POST \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  -H "Content-Type: application/json" \
  -d "{
    \"transaction\": {
      \"action\": \"Action::\\\"Transfer\\\"\",
      \"schema\": \"BITCOIN_TX_V2\",
      \"payload\": \"${PAYLOAD_B64}\",
      \"digest\": \"${DIGEST_B64}\"
    },
    \"authorizationInfo\": {
      \"signature\": \"${SIGNER_1_SIG_B64}\",
      \"signedPayload\": \"${SIGNED_DECISION_PAYLOAD_B64}\"
    },
    \"requestId\": \"11111111-1111-4111-8111-111111111604\"
  }" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions")

echo "$RESPONSE" | jq
export TRANSACTION_NAME=$(echo "$RESPONSE" | jq -r '.name')

Erwartete Antwort: state: PENDING_AUTHORIZATION und policyEvaluationRecord.decision: PERMIT.

🕵️‍♂️ Transaktionsstatus prüfen (Polling)

Fragen Sie den Transaktionsstatus ab, indem Sie den folgenden Befehl regelmäßig ausführen:

curl

curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq

B. Entscheidung des Unterzeichners aufzeichnen (autorisieren)

Da CreateTransaction in Schritt 6.A bereits die Entscheidung von Signer 1 (AUTHORIZE) aufgezeichnet hat (1 von 2 erforderlichen Entscheidungen gemäß dem 2-von-3-KeyPolicy-Schwellenwert), muss nur ein weiterer Mitunterzeichner, der einen Schlüsselfragment besitzt, die Transaktion mit transactions.recordSignerDecision (RecordSignerDecision) autorisieren.

Unterzeichnen Sie die TransactionSigningPayload, in der transaction (${TRANSACTION_NAME}) genannt wird, und rufen Sie recordSignerDecision als Unterzeichner 2 (bob@example.com) auf:

curl

# 1. Check state for Signer 2
TX_INFO=$(curl -s -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}")

TX_STATE=$(echo "$TX_INFO" | jq -r '.state // "UNKNOWN"')

if [ "$TX_STATE" == "PENDING_AUTHORIZATION" ]; then
  # 2. Sign as Signer 2 naming transaction ($TRANSACTION_NAME)
  SIGNER_2_SIG_B64=$(sign_tx_decision "signer-bob" "transaction" "${TRANSACTION_NAME}" "AUTHORIZE" "${DIGEST_HEX}")
  SIGNER_2_PAYLOAD_B64=$(openssl base64 -A -in tx_decision_payload.bin)

  # 3. Call RecordSignerDecision as Signer 2
  echo "Recording AUTHORIZE decision for Signer 2..."
  curl -s -X POST \
    -H "Authorization: Bearer $(gcloud auth print-access-token "bob@example.com")" \
    -H "X-Goog-User-Project: my-project" \
    -H "Content-Type: application/json" \
    -d "{
      \"decision\": \"AUTHORIZE\",
      \"authorizationInfo\": {
        \"signature\": \"${SIGNER_2_SIG_B64}\",
        \"signedPayload\": \"${SIGNER_2_PAYLOAD_B64}\"
      }
    }" \
    "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}:recordSignerDecision" | jq
elif [ "$TX_STATE" == "SIGNING" ] || [ "$TX_STATE" == "SIGNING_SUCCEEDED" ]; then
  echo "Transaction is in state $TX_STATE. Skipping Signer 2."
else
  echo "Transaction is in an unexpected state: $TX_STATE"
fi

C. Endgültige Signatur abrufen

Sobald der Schwellenwert für Autorisierungen erreicht ist, prüfen Sie den Transaktionsstatus und rufen Sie finalSignature (und recoveryId für EC_SIGN_SECP256K1-Schlüssel) ab:

curl

curl -s -X GET \
  -H "Authorization: Bearer $(gcloud auth print-access-token "alice@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq

Wenn state den Wert SIGNING_SUCCEEDED hat, kopieren Sie das Feld finalSignature aus der zurückgegebenen JSON-Datei für Schritt 7.


Schritt 7: Transaktion an die Blockchain übertragen

Sobald Ihre Transaktion den Status SIGNING_SUCCEEDED erreicht hat und Sie die finalSignature abgerufen haben, müssen Sie im letzten Schritt die signierte Wire-Transaktion erstellen und an das Netzwerk senden.

Ethereum

Ableitung und Übertragung von Wiederherstellungs-IDs

Die MPC API gibt eine unformatierte 64-Byte-ECDSA-Signatur in finalSignature zurück (bestehend aus den 32-Byte-Werten r und s in kanonischer Low-S-Form gemäß EIP-2) sowie das Wiederherstellungs-Paritätsbit (0 oder 1) in recoveryId.

Das folgende Python-Skript erstellt die signierte Transaktion mit Ihrem finalSignature, verifiziert die wiederhergestellte Absenderadresse anhand der Ethereum-Adresse Ihres MPC-Schlüssels und sendet sie an das Netzwerk.

Skript für die Nachricht an alle:

Führen Sie den folgenden Block in Ihrem Terminal aus, um broadcast_eth.py zu schreiben und auszuführen:

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

Häufige Fehler beheben

  • invalid sender: recovery failed oder invalid transaction v, r, s values: Dieser Fehler tritt auf, wenn die Transaktionsparameter in Ihrem Skript nicht genau mit der signierten Nutzlast übereinstimmen. Achten Sie darauf, dass die Felder value, nonce und to mit den in Schritt 6 verarbeiteten Feldern übereinstimmen.
  • insufficient funds for gas * price + value: Ihre Transaktion ist kryptografisch gültig und wurde vom Knoten akzeptiert, aber die Ethereum-Adresse Ihres MPC-Schlüssels hat nicht genügend Testnetz-ETH, um den Vorgang abzudecken. Sie müssen Ihre Adresse vor dem Senden mit Guthaben aufladen.

Bitcoin

SegWit-Wire-Transaktionserstellung und -übertragung

Bei Bitcoin Native SegWit-Transaktionen (BIP 141) muss die unformatierte 64-Byte-(r, s)-MPC-Signatur in kanonisches Low-S-ASN.1-DER formatiert, mit 0x01 (SIGHASH_ALL) angehängt und mit dem komprimierten öffentlichen 33-Byte-Schlüssel in den SegWit-Witness-Stack mit zwei Elementen eingefügt werden: [DER signature || 0x01, compressed public key].

Skript für die Nachricht an alle:

Führen Sie den folgenden Block aus, um broadcast_btc.py zu erstellen und auszuführen:

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

Häufige Fehler beheben

  • mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation) / non-mandatory-script-verify-flag (Non-canonical DER signature): Die s-Komponente der Signatur muss im kanonischen Low-S-Format (s <= N/2) und ordnungsgemäß DER-codiert sein.
  • bad-txns-in-belowout / bad-txns-fee-negative: Die Gesamtausgaben übersteigen den Eingabewert. Die Änderungsberechnung (InputSatoshis - PaymentSatoshis - MinerFee) muss strikt positiv sein.

📖 Anhang: Betriebshandbuch (Abfragen und Prüfen)

Mit diesen Befehlen können Sie den Status Ihrer Ressourcen jederzeit prüfen und verwalten.


🏢 1. Instanzen

MPC-Instanzen in Ihrem Projekt und an Ihrem Standort prüfen und verwalten.

A. Alle Instanzen auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/projects/my-project/locations/us-central1/instances" | jq

B. Details zu einer bestimmten Instanz abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "https://mpc.googleapis.com/v1beta/${INSTANCE_NAME}" | jq

👥 2. Teams und Nutzer

Teams und Nutzer in Ihrer Instanz prüfen

A. Alle Teams auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/teams" | jq

B. Details zu einem bestimmten Team abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TEAM_NAME}" | jq

C. Alle registrierten Nutzer auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/users" | jq

D. Details zu einem bestimmten Nutzer abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${SIGNER_1_NAME}" | jq

📜 3. Transaktionsrichtlinien und KeyLists

Prüfen Sie die Transaktionsrichtlinien und Schlüssellisten.

A. Alle Transaktionsrichtlinien auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/transactionPolicies" | jq

B. Details zu einer bestimmten Transaktionsrichtlinie abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${POLICY_NAME}" | jq

C. KeyLists auflisten und abrufen

curl

# List all KeyLists
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyLists" | jq

# Get a specific KeyList
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_LIST_NAME}" | jq

🔑 4. Schlüssel und Schlüsselbunde

Schlüsselbunde und verteilte Schlüssel prüfen.

A. Alle Schlüsselbunde auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings" | jq

B. Alle Schlüssel in einem Schlüsselbund auflisten

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_RING_NAME}/keys" | jq

C. Details eines bestimmten verteilten Schlüssels abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}" | jq

📝 5. Transaktionen (Signaturvorgänge)

Transaktionen und Signaturstatus prüfen.

A. Alle Transaktionen für einen Schlüssel (oder alle Schlüssel) auflisten

curl

# List transactions for a specific Key
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${KEY_NAME}/transactions" | jq

# List transactions across all KeyRings and Keys using the '-' wildcard
curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${INSTANCE_NAME}/keyRings/-/keys/-/transactions" | jq

B. Details oder Status einer bestimmten Transaktion abrufen

curl

curl -H "Authorization: Bearer $(gcloud auth print-access-token "admin-1@example.com")" \
  -H "X-Goog-User-Project: my-project" \
  "${INSTANCE_URL}/v1beta/${TRANSACTION_NAME}" | jq