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 (
EnableUserRoleundApproveRoleChange) 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 (CreateTransactionundRecordSignerDecision) erforderlich sind, um eine Signatur zu autorisieren. - Transaktionsrichtlinien (
TransactionPolicyundKeyList): Ein Quorum von Richtlinienautoren (POLICY_AUTHOR) schlägt Cedar-permit- undforbid-Regeln (TransactionPolicy) sowieKeyList-Adressen-Zulassungslisten vor, genehmigt sie oder deaktiviert sie. JedeCreateTransaction-Anfrage wird vor dem Signieren anhand aktiver Transaktionsrichtlinien (PERMIToderDENY) ausgewertet.
Vorbereitung
Prüfen Sie zuerst, ob Sie Folgendes haben:
- Ein Google Cloud -Projekt. Informationen zum Erstellen eines Projekts finden Sie unter Projekte erstellen.
- Wenn Sie Cloud Shell nicht verwenden, muss die
gcloud-Befehlszeile installiert und authentifiziert sein. 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.comSo rufen Sie die aktivierten APIs auf:
gcloud services list --enabledPython 3.9 oder höher und die Hilfsbibliotheken für die Anleitung sind installiert:
pip install embit eth-account eth-utils requests(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
ADMINundPOLICY_AUTHORautorisieren undKeys erstellen. - Richtlinienautoren: Governance-Richtlinien autorisieren (
TransactionPolicy). - Unterzeichner: Erteilen die Autorisierung (
AUTHORIZEoderDENY) 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:
- Die Entität (Signierer oder Richtlinienautor) generiert ihr Schlüsselpaar lokal und überträgt ihren öffentlichen Schlüssel sicher an den Administrator.
- Der Administrator weist dem Hauptkonto des Nutzers die entsprechende IAM-Rolle für das Projekt zu (
roles/mpc.editorfür Signierer und Richtlinienautoren;roles/mpc.adminfür Administratoren) und ruft die MPC API auf, um dieprincipalundauthorizationKeydes 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.editorgewährt und sie werden mitroles: ["SIGNER"]undteam: "$TEAM_NAME"erstellt. Die RolleSIGNERwird sofort zuACTIVE, ohne dass die Genehmigung eines Administrator-Quorums erforderlich ist, da die RolleSIGNERallein keinen Zugriff auf vorhandene Schlüssel gewährt. Ein Signer erhält nur dann Schlüsselfreigaben, wenn er bei der Schlüsselgenerierung in derKeyPolicyeinesKeyRingenthalten ist. Richtlinienautoren: Ihnen wird
roles/mpc.editorin IAM gewährt und sie werden ohne anfängliche Rollen und ohneteamerstellt. Anschließend wird ihnen in Schritt 3.C die RollePOLICY_AUTHORmit der Genehmigung des Administrator-Quorums gewährt (daPOLICY_AUTHORundADMINinstanzweite 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:
- Admin 1 ruft
users.enableRole(EnableUserRole) auf, wodurch die Rolle zuPENDING_ACTIVEverschoben und die Signatur von Admin 1 als erste Genehmigung aufgezeichnet wird. - Admin 2 ruft
users.approveRoleChange(ApproveRoleChange) mit seiner Signatur auf und überträgt die Rolle anACTIVE.
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:
- 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. - 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. - 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:
- Zahlen Sie Guthaben auf Ihre abgeleitete Native SegWit-Adresse ein (z. B. über ein Testnetz-Faucet).
- Fragen Sie die nicht ausgegebenen UTXOs Ihrer Adresse mit Ihrem Bitcoin-Knoten (
bitcoin-cli listunspent 1 9999999 "[\"tb1q...\"]") oder einer Block-Explorer-API ab. - 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.0oder1).value: Der nicht ausgegebene Betrag in Satoshis (z. B.200000Satoshis = 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 failedoderinvalid 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 Feldervalue,nonceundtomit 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): Dies-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