mpcvm: custody answers to the transport, not to the request body

Signing authority was resolved from a name the requester wrote about itself.
CrossChainRequest already receives the sender's chain id from the transport,
which authenticated it, and then passed req.RequestingChain to the permission
lookup instead -- so the proven identity sat unused in the signature while a
field the sender controlled decided what it was allowed to do. The stock table
grants B-Chain sign, keygen and reshare at 100k/day, so the claim was worth
having.

Caller carries an authenticated chain and nothing else can be substituted for
it: the only constructor takes the transport's chain id, the fields are
unexported, and the zero value grants nothing, so a path that forgets to
authenticate fails closed instead of quietly inheriting rights. Making it a type
rather than a convention is what turns "remember not to read the body" into
something the compiler checks -- it listed every call site that had been passing
a claim.

Authorization binds to the chain ID, never to the label. A label is an
operator's convenience and anyone can spell it; the id is the identity consensus
itself uses. An entry with no chainId is bound to no chain and grants nothing,
and Initialize names those entries at boot, because silence there would read as
"custody is configured" right up until the first release failed.

HTTP keygen and sign are gone rather than guarded. That transport authenticates
no chain, so there was no check to write -- a body can say it is the bridge as
easily as it can say anything else. Ceremonies are requested by chains over the
cross-chain path; the HTTP surface reads replicated state, which needs no
identity.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-20 10:47:08 -07:00
co-authored by Hanzo Dev
parent a449465dc6
commit b9f95308a8
8 changed files with 227 additions and 91 deletions
+4 -5
View File
@@ -98,7 +98,6 @@ type BridgeTransferAttestation struct {
// key must sign). Everything the digest commits to travels here; nothing else
// can be minted from the resulting attestation.
type BridgeReleaseRequest struct {
RequestingChain string `json:"requestingChain"` // authorised chain id in M's permission table (e.g. "B-Chain")
// KeyID names the custody key that must sign. It is REQUIRED: there is no
// "active key" for a request to fall back to, because a fallback means the
// chain, not the requester, chose which vault to spend from — and a key
@@ -121,8 +120,8 @@ type BridgeReleaseRequest struct {
// The signature is a standard secp256k1 ECDSA signature over the domain-bound
// digest, so a destination-chain gateway contract verifies it exactly like a
// single-key signature (ecrecover to the custody address).
func (vm *VM) RequestBridgeRelease(ctx context.Context, req BridgeReleaseRequest) (*BridgeTransferAttestation, error) {
return vm.AttestBridgeTransfer(ctx, req.RequestingChain, req.KeyID, BridgeTransfer{
func (vm *VM) RequestBridgeRelease(ctx context.Context, by Caller, req BridgeReleaseRequest) (*BridgeTransferAttestation, error) {
return vm.AttestBridgeTransfer(ctx, by, req.KeyID, BridgeTransfer{
SrcChainID: req.SrcChainID,
DstChainID: req.DstChainID,
Asset: req.Asset,
@@ -143,13 +142,13 @@ func (vm *VM) RequestBridgeRelease(ctx context.Context, req BridgeReleaseRequest
// (Ceremony(id), against consensus state).
func (vm *VM) AttestBridgeTransfer(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
bt BridgeTransfer,
) (*BridgeTransferAttestation, error) {
digest := bt.Digest()
op, err := vm.RequestSignature(ctx, requestingChain, keyID, digest[:])
op, err := vm.RequestSignature(ctx, by, keyID, digest[:])
if err != nil {
return nil, fmt.Errorf("mpcvm: bridge attestation for %s: %w", keyID, err)
}
+2 -2
View File
@@ -56,11 +56,11 @@ func init() {
// same node at a LATER block is the legitimate way ownership changes hands.
func (vm *VM) AttestNFTOwnership(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
claim ownership.Claim,
) (*ownership.Attestation, error) {
qa, err := vm.attest(ctx, requestingChain, keyID, DomainNFTOwnership, claim.Subject(), claim.Root(), claim.Block)
qa, err := vm.attest(ctx, by, keyID, DomainNFTOwnership, claim.Subject(), claim.Root(), claim.Block)
if err != nil {
return nil, fmt.Errorf("mpcvm: attest ownership of token %d for node %x: %w", claim.Token, claim.Node, err)
}
+5 -7
View File
@@ -246,7 +246,7 @@ func TestBridgeCustody_KeygenSignAndRecordOverGossip(t *testing.T) {
require.Equal(t, "2-of-3", policy.String())
keygens := runOnAll(t, ctx, vms, func(ctx context.Context, vm *VM) (*Operation, error) {
return vm.StartKeygen(ctx, keyID, "B-Chain")
return vm.StartKeygen(ctx, keyID, authenticated(vm, "B-Chain"))
})
first := keygens[0]
@@ -288,7 +288,6 @@ func TestBridgeCustody_KeygenSignAndRecordOverGossip(t *testing.T) {
var recip [20]byte
copy(recip[:], []byte("recipient-0xabc0123"))
req := BridgeReleaseRequest{
RequestingChain: "B-Chain",
KeyID: keyID,
SrcChainID: 200201, // Zoo EVM
DstChainID: 97368, // Lux testnet M-Chain route
@@ -302,7 +301,7 @@ func TestBridgeCustody_KeygenSignAndRecordOverGossip(t *testing.T) {
// key that is two of the three, chosen deterministically from the task, so
// the third declines without anyone coordinating that.
atts, declined := runOnQuorum(t, ctx, vms, func(ctx context.Context, vm *VM) (*BridgeTransferAttestation, error) {
return vm.RequestBridgeRelease(ctx, req)
return vm.RequestBridgeRelease(ctx, authenticated(vm, "B-Chain"), req)
})
require.Len(t, atts, policy.K, "exactly K validators sign")
require.Equal(t, policy.N-policy.K, declined, "the rest decline rather than sign")
@@ -349,7 +348,7 @@ func TestBridgeCustody_KeygenSignAndRecordOverGossip(t *testing.T) {
// Replay is refused: the ceremony id is derived from (key, digest, signers),
// so asking again for the identical release is the identical ceremony — and
// recording it twice would be a double release.
_, err := signerVM(t, vms, atts[0]).RequestBridgeRelease(ctx, req)
_, err := signerVM(t, vms, atts[0]).RequestBridgeRelease(ctx, authenticated(signerVM(t, vms, atts[0]), "B-Chain"), req)
require.ErrorIs(t, err, ErrCeremonyExists)
// Proof the fabric actually carried ceremony messages between validators
@@ -483,7 +482,7 @@ func TestBridgeCustody_ThreeOfFive(t *testing.T) {
// --- DKG at degree 2 across all five ---
keygens := runOnAll(t, ctx, vms, func(ctx context.Context, vm *VM) (*Operation, error) {
return vm.StartKeygenWithPolicy(ctx, keyID, policy, "B-Chain")
return vm.StartKeygenWithPolicy(ctx, keyID, policy, authenticated(vm, "B-Chain"))
})
rec := keygens[0].Key
require.Equal(t, policy, rec.Policy)
@@ -503,7 +502,6 @@ func TestBridgeCustody_ThreeOfFive(t *testing.T) {
var recip [20]byte
copy(recip[:], []byte("recipient-3of5"))
req := BridgeReleaseRequest{
RequestingChain: "B-Chain",
KeyID: keyID,
SrcChainID: 200201,
DstChainID: 97368,
@@ -517,7 +515,7 @@ func TestBridgeCustody_ThreeOfFive(t *testing.T) {
// ErrNotInQuorum. Nobody coordinates that — each node computes the same
// quorum from the task alone.
signed, declined := runOnQuorum(t, ctx, vms, func(ctx context.Context, vm *VM) (*BridgeTransferAttestation, error) {
return vm.RequestBridgeRelease(ctx, req)
return vm.RequestBridgeRelease(ctx, authenticated(vm, "B-Chain"), req)
})
require.Len(t, signed, 3, "exactly K=3 validators must sign a 3-of-5 key")
require.Equal(t, 2, declined, "the remaining N-K=2 validators must decline, not sign")
+59
View File
@@ -0,0 +1,59 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
import (
"fmt"
"github.com/luxfi/ids"
)
// Caller is a chain whose identity the TRANSPORT authenticated.
//
// It cannot be built from a request body. The only constructor takes the chain
// id the transport supplies, so "who is asking" is never a field the asker
// writes about itself. A name in a payload is a claim, and a claim its own
// subject authored proves nothing — the same reason a node's ownership
// attestation is read out of M-Chain consensus state and not out of the node's
// own config, where its operator could write whatever it liked.
//
// The zero Caller holds no permissions and can do nothing, so a path that
// forgets to authenticate fails closed rather than silently acquiring rights.
type Caller struct {
name string
perms *ChainPermissions
}
// Name is the operator's label for the authenticated chain, used for
// attribution and quota accounting. It is an OUTPUT of authentication, never an
// input to it.
func (c Caller) Name() string { return c.name }
// caller resolves the permissions of the chain the transport authenticated as
// sender.
//
// Authorization binds to the chain ID, never to a name. A name is an operator's
// label — convenient in a config file, and worth exactly nothing as evidence,
// because anyone can spell it. A chain id is the identity consensus itself
// uses.
//
// A permission entry carrying no ChainID is bound to no chain and therefore
// authorizes nobody. That is deliberate: custody is granted to a specific
// chain by an operator who names it, and an entry that matched on its label
// alone would hand custody to whoever typed the label. An unbound entry refuses
// and says so, because "this chain may not sign" and "this deployment never
// bound the chain" are different operational problems.
func (vm *VM) caller(sender ids.ID) (Caller, error) {
vm.mu.RLock()
defer vm.mu.RUnlock()
for name, p := range vm.config.AuthorizedChains {
if p == nil || p.ChainID == "" {
continue
}
if p.ChainID == sender.String() {
return Caller{name: name, perms: p}, nil
}
}
return Caller{}, fmt.Errorf("%w: no permission entry is bound to chain %s", ErrUnauthorizedChain, sender)
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (C) 2019-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package mpcvm
import (
"context"
"errors"
"testing"
"github.com/luxfi/ids"
)
// authenticated builds the Caller the TRANSPORT would produce for a chain that
// is bound to a permission entry. In-package tests may build one because they
// stand in for the transport; a request body cannot, which is the whole point.
func authenticated(vm *VM, name string) Caller {
return Caller{name: name, perms: vm.config.AuthorizedChains[name]}
}
// TestCaller_ZeroValueMaySignNothing: a path that forgets to authenticate must
// fail closed rather than quietly acquire whatever the zero value implies.
func TestCaller_ZeroValueMaySignNothing(t *testing.T) {
vm := &VM{}
if _, err := vm.RequestSignature(context.Background(), Caller{}, "key", make([]byte, 32)); !errors.Is(err, ErrUnauthorizedChain) {
t.Fatalf("the zero Caller must authorize nothing, got %v", err)
}
if _, err := vm.StartKeygenWithPolicy(context.Background(), "key", vm.Policy(), Caller{}); !errors.Is(err, ErrUnauthorizedChain) {
t.Fatalf("the zero Caller must not run keygen, got %v", err)
}
}
// TestCaller_UnboundEntryAuthorizesNobody is the fail-closed default. The stock
// permission table ships names with no chain id, so until an operator binds a
// name to a chain, no chain id resolves — including one that happens to spell
// itself like an entry.
func TestCaller_UnboundEntryAuthorizesNobody(t *testing.T) {
vm := &VM{config: ThresholdConfig{AuthorizedChains: map[string]*ChainPermissions{
"B-Chain": {ChainID: "", CanSign: true},
}}}
if _, err := vm.caller(ids.GenerateTestID()); !errors.Is(err, ErrUnauthorizedChain) {
t.Fatalf("an entry bound to no chain must authorize nobody, got %v", err)
}
}
// TestCaller_BindsToChainIDNotName: authorization follows the chain id the
// transport authenticated. A second entry whose LABEL is more appealing must
// not win, and an id that matches nothing must not fall back to a label.
func TestCaller_BindsToChainIDNotName(t *testing.T) {
bridge := ids.GenerateTestID()
stranger := ids.GenerateTestID()
vm := &VM{config: ThresholdConfig{AuthorizedChains: map[string]*ChainPermissions{
"B-Chain": {ChainID: bridge.String(), CanSign: true},
}}}
c, err := vm.caller(bridge)
if err != nil {
t.Fatalf("the bound chain must authorize: %v", err)
}
if c.Name() != "B-Chain" {
t.Fatalf("resolved name = %q, want B-Chain", c.Name())
}
if _, err := vm.caller(stranger); !errors.Is(err, ErrUnauthorizedChain) {
t.Fatalf("an unbound chain id must not resolve, got %v", err)
}
}
// TestCaller_PayloadCannotNameItself is the defect this type exists to prevent.
//
// CrossChainRequest receives the sender's chain id from the transport AND a
// body in which the sender describes itself. Authorization must read the first
// and ignore the second, so a peer that writes "B-Chain" into its payload gains
// nothing it did not already have.
func TestCaller_PayloadCannotNameItself(t *testing.T) {
bridge := ids.GenerateTestID()
impostor := ids.GenerateTestID()
vm := &VM{config: ThresholdConfig{AuthorizedChains: map[string]*ChainPermissions{
"B-Chain": {ChainID: bridge.String(), CanSign: true, CanKeygen: true},
}}}
// The impostor's id is bound to nothing; its claim to be B-Chain is in the
// only place it controls — the body — and must not be consulted.
if _, err := vm.caller(impostor); !errors.Is(err, ErrUnauthorizedChain) {
t.Fatal("a self-declared name must not produce a Caller")
}
}
+10 -47
View File
@@ -111,10 +111,16 @@ func (vm *VM) handleRPCMethod(ctx context.Context, method string, params json.Ra
switch method {
// Ceremonies. Both run to completion before returning: keygen yields a
// registered key, sign yields the signature itself.
case "threshold_keygen":
return vm.rpcKeygen(ctx, params)
case "threshold_sign":
return vm.rpcSign(ctx, params)
case "threshold_keygen", "threshold_sign":
// Ceremonies are requested BY a chain, and this transport authenticates
// no chain: an HTTP body can say it is the bridge as easily as it can
// say anything else. Custody rights are granted over the cross-chain
// path, where the sender's chain id is authenticated (CrossChainRequest),
// so there is exactly one way in and it is not this one.
return nil, &RPCError{
Code: RPCErrorUnauthorized,
Message: fmt.Sprintf("%s is requested by a chain, over the cross-chain transport that authenticates it; this endpoint authenticates no caller", method),
}
// Custody registry (replicated state)
case "mpc_getKey":
@@ -205,31 +211,6 @@ type KeygenResult struct {
Key KeyInfo `json:"key"`
}
func (vm *VM) rpcKeygen(ctx context.Context, params json.RawMessage) (*KeygenResult, error) {
var p KeygenParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
var (
op *Operation
err error
)
if p.Policy.Valid() {
op, err = vm.StartKeygenWithPolicy(ctx, p.KeyID, p.Policy, p.RequestedBy)
} else {
op, err = vm.StartKeygen(ctx, p.KeyID, p.RequestedBy)
}
if err != nil {
return nil, asRPCError(err)
}
return &KeygenResult{
Ceremony: ceremonyInfoOf(pendingRecord(op)),
Key: keyInfoOf(op.Key),
}, nil
}
// SignParams contains parameters for signing.
type SignParams struct {
KeyID string `json:"keyId"`
@@ -240,24 +221,6 @@ type SignParams struct {
RequestingChain string `json:"requestingChain"`
}
func (vm *VM) rpcSign(ctx context.Context, params json.RawMessage) (*CeremonyInfo, error) {
var p SignParams
if err := json.Unmarshal(params, &p); err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: err.Error()}
}
messageHash, err := hex.DecodeString(stripHexPrefix(p.MessageHash))
if err != nil {
return nil, &RPCError{Code: RPCErrorInvalidParams, Message: "messageHash is not hex"}
}
op, err := vm.RequestSignature(ctx, p.RequestingChain, p.KeyID, messageHash)
if err != nil {
return nil, asRPCError(err)
}
info := ceremonyInfoOf(pendingRecord(op))
return &info, nil
}
// =============================================================================
// Custody registry RPCs (replicated state)
// =============================================================================
+9 -9
View File
@@ -51,12 +51,12 @@ import (
// handle to an unfinished ceremony is a handle to state that only one node has.
type ThresholdService interface {
// StartKeygen runs a distributed key-generation ceremony for keyID under
// the chain's default policy, attributed to requestedBy.
StartKeygen(ctx context.Context, keyID, requestedBy string) (*Operation, error)
// the chain's default policy, attributed to the AUTHENTICATED caller.
StartKeygen(ctx context.Context, keyID string, by Caller) (*Operation, error)
// StartKeygenWithPolicy runs DKG under an explicit k-of-n policy. The
// polynomial degree is derived from the policy, never passed alongside it.
StartKeygenWithPolicy(ctx context.Context, keyID string, policy quorum.Policy, requestedBy string) (*Operation, error)
StartKeygenWithPolicy(ctx context.Context, keyID string, policy quorum.Policy, by Caller) (*Operation, error)
// Policy returns the chain's default signing policy.
Policy() quorum.Policy
@@ -86,9 +86,9 @@ type MPCService interface {
ThresholdService
// RequestSignature asks the custody committee for keyID to threshold-sign
// messageHash on behalf of requestingChain. It returns when the ceremony
// messageHash on behalf of the AUTHENTICATED caller. It returns when the ceremony
// has produced a signature that verifies under the registered group key.
RequestSignature(ctx context.Context, requestingChain, keyID string, messageHash []byte) (*Operation, error)
RequestSignature(ctx context.Context, by Caller, keyID string, messageHash []byte) (*Operation, error)
// Ceremony returns one recorded ceremony — the replicated, durable evidence
// that a signature was produced, including the signature. Ceremonies
@@ -102,18 +102,18 @@ type MPCService interface {
// RequestBridgeRelease is the B→M seam: a bridge release request in, a
// threshold-signed self-describing attestation out.
RequestBridgeRelease(ctx context.Context, req BridgeReleaseRequest) (*BridgeTransferAttestation, error)
RequestBridgeRelease(ctx context.Context, by Caller, req BridgeReleaseRequest) (*BridgeTransferAttestation, error)
// AttestOracleCommit produces a threshold attestation over an oracle
// read/write commitment for requestingChain.
AttestOracleCommit(ctx context.Context, requestingChain, keyID string, requestID [32]byte, kind uint8, commitRoot [32]byte, epoch uint64) (*QuantumAttestation, error)
AttestOracleCommit(ctx context.Context, by Caller, keyID string, requestID [32]byte, kind uint8, commitRoot [32]byte, epoch uint64) (*QuantumAttestation, error)
// AttestSessionComplete attests that a bridge/custody session finished with
// the given output/oracle/receipts roots.
AttestSessionComplete(ctx context.Context, requestingChain, keyID string, sessionID [32]byte, outputHash, oracleRoot, receiptsRoot [32]byte, epoch uint64) (*QuantumAttestation, error)
AttestSessionComplete(ctx context.Context, by Caller, keyID string, sessionID [32]byte, outputHash, oracleRoot, receiptsRoot [32]byte, epoch uint64) (*QuantumAttestation, error)
// AttestEpochBeacon produces the per-epoch beacon attestation.
AttestEpochBeacon(ctx context.Context, requestingChain, keyID string, epoch uint64, previousRef [32]byte) (*QuantumAttestation, error)
AttestEpochBeacon(ctx context.Context, by Caller, keyID string, epoch uint64, previousRef [32]byte) (*QuantumAttestation, error)
// VerifyAttestation verifies a QuantumAttestation against this node's
// custody registry.
+52 -21
View File
@@ -11,6 +11,7 @@
package mpcvm
import (
"sort"
"context"
"crypto/sha256"
"encoding/binary"
@@ -318,12 +319,31 @@ func (vm *VM) Initialize(
return fmt.Errorf("mpcvm: load key shares: %w", err)
}
bound, unbound := 0, make([]string, 0, len(vm.config.AuthorizedChains))
for name, p := range vm.config.AuthorizedChains {
if p != nil && p.ChainID != "" {
bound++
continue
}
unbound = append(unbound, name)
}
sort.Strings(unbound)
vm.log.Info("M-Chain initialized",
log.String("policy", vm.config.Policy.String()),
log.String("party", string(vm.partyID)),
log.Int("sharesHeld", len(vm.shares)),
log.Int("authorizedChains", len(vm.config.AuthorizedChains)),
log.Int("authorizedChains", bound),
)
if len(unbound) > 0 {
// Say it plainly. A permission entry names a chain by label, and a label
// is not an identity — until an operator sets chainId, the entry grants
// custody to nobody. Silence here would read as "custody is configured"
// right up until the first release fails.
vm.log.Warn("custody entries name a chain but bind to none — set chainId on each to grant it",
log.Strings("chains", unbound),
)
}
return nil
}
@@ -467,8 +487,8 @@ func (vm *VM) parseConfig(configBytes []byte) error {
// chain. It is gone.
// StartKeygen generates a custody key using the chain's default policy.
func (vm *VM) StartKeygen(ctx context.Context, keyID, requestedBy string) (*Operation, error) {
return vm.StartKeygenWithPolicy(ctx, keyID, vm.Policy(), requestedBy)
func (vm *VM) StartKeygen(ctx context.Context, keyID string, by Caller) (*Operation, error) {
return vm.StartKeygenWithPolicy(ctx, keyID, vm.Policy(), by)
}
// StartKeygenWithPolicy generates a custody key under an explicit k-of-n policy.
@@ -476,11 +496,11 @@ func (vm *VM) StartKeygen(ctx context.Context, keyID, requestedBy string) (*Oper
// The policy is a quorum.Policy, not a pair of ints, so a caller cannot express
// the quorum ambiguously: "3-of-5" is the only spelling, and the polynomial
// degree is derived from it inside RunKeygen.
func (vm *VM) StartKeygenWithPolicy(ctx context.Context, keyID string, policy quorum.Policy, requestedBy string) (*Operation, error) {
perms, ok := vm.permissions(requestedBy)
if !ok || !perms.CanKeygen {
return nil, fmt.Errorf("%w: %s may not request key generation", ErrUnauthorizedChain, requestedBy)
func (vm *VM) StartKeygenWithPolicy(ctx context.Context, keyID string, policy quorum.Policy, by Caller) (*Operation, error) {
if by.perms == nil || !by.perms.CanKeygen {
return nil, fmt.Errorf("%w: %s may not request key generation", ErrUnauthorizedChain, by.name)
}
requestedBy := by.name
ctx, cancel := context.WithTimeout(ctx, vm.sessionTimeout())
defer cancel()
op, err := vm.RunKeygen(ctx, keyID, policy, requestedBy)
@@ -495,11 +515,12 @@ func (vm *VM) StartKeygenWithPolicy(ctx context.Context, keyID string, policy qu
// RequestSignature produces a threshold signature over messageHash with a
// registered custody key and stages it for the next block.
func (vm *VM) RequestSignature(ctx context.Context, requestingChain, keyID string, messageHash []byte) (*Operation, error) {
perms, ok := vm.permissions(requestingChain)
if !ok || !perms.CanSign {
return nil, fmt.Errorf("%w: %s may not request signatures", ErrUnauthorizedChain, requestingChain)
func (vm *VM) RequestSignature(ctx context.Context, by Caller, keyID string, messageHash []byte) (*Operation, error) {
perms := by.perms
if perms == nil || !perms.CanSign {
return nil, fmt.Errorf("%w: %s may not request signatures", ErrUnauthorizedChain, by.name)
}
requestingChain := by.name
if perms.MaxSigningSize > 0 && len(messageHash) > perms.MaxSigningSize {
return nil, fmt.Errorf("message too large: %d > %d", len(messageHash), perms.MaxSigningSize)
}
@@ -857,12 +878,22 @@ func (vm *VM) CrossChainRequest(ctx context.Context, chainID ids.ID, requestID u
return err
}
// Authorize from the chain id the TRANSPORT authenticated, never from
// req.RequestingChain. That field is written by the sender about itself, so
// reading it here would let any peer that can reach this method claim any
// chain's custody rights; it survives only as attribution on the recorded
// operation, where it is set from the authenticated name below.
by, err := vm.caller(chainID)
if err != nil {
return err
}
switch req.Type {
case "sign":
_, err := vm.RequestSignature(ctx, req.RequestingChain, req.KeyID, req.MessageHash)
_, err := vm.RequestSignature(ctx, by, req.KeyID, req.MessageHash)
return err
case "keygen":
_, err := vm.StartKeygen(ctx, req.KeyID, req.RequestingChain)
_, err := vm.StartKeygen(ctx, req.KeyID, by)
return err
default:
return fmt.Errorf("mpcvm: unknown cross-chain request type %q", req.Type)
@@ -1156,7 +1187,7 @@ func ComputeAttestationPayload(domain AttestationDomain, subjectID, commitmentRo
// between an oracle commit, a session completion and an epoch beacon.
func (vm *VM) attest(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
domain AttestationDomain,
subjectID [32]byte,
@@ -1165,7 +1196,7 @@ func (vm *VM) attest(
) (*QuantumAttestation, error) {
payload := ComputeAttestationPayload(domain, subjectID, commitRoot, epoch)
op, err := vm.RequestSignature(ctx, requestingChain, keyID, payload[:])
op, err := vm.RequestSignature(ctx, by, keyID, payload[:])
if err != nil {
return nil, fmt.Errorf("mpcvm: attest %s: %w", domain, err)
}
@@ -1201,7 +1232,7 @@ func (vm *VM) attest(
// AttestOracleCommit creates a threshold attestation for an oracle commitment.
func (vm *VM) AttestOracleCommit(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
requestID [32]byte,
kind uint8, // 0 = write, 1 = read
@@ -1212,13 +1243,13 @@ func (vm *VM) AttestOracleCommit(
if kind != 0 {
domain = DomainOracleRead
}
return vm.attest(ctx, requestingChain, keyID, domain, requestID, commitRoot, epoch)
return vm.attest(ctx, by, keyID, domain, requestID, commitRoot, epoch)
}
// AttestSessionComplete creates a threshold attestation for session completion.
func (vm *VM) AttestSessionComplete(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
sessionID [32]byte,
outputHash [32]byte,
@@ -1233,13 +1264,13 @@ func (vm *VM) AttestSessionComplete(
var commitRoot [32]byte
copy(commitRoot[:], h.Sum(nil))
return vm.attest(ctx, requestingChain, keyID, DomainSessionComplete, sessionID, commitRoot, epoch)
return vm.attest(ctx, by, keyID, DomainSessionComplete, sessionID, commitRoot, epoch)
}
// AttestEpochBeacon creates a threshold attestation for epoch beacon randomness.
func (vm *VM) AttestEpochBeacon(
ctx context.Context,
requestingChain string,
by Caller,
keyID string,
epoch uint64,
previousRef [32]byte,
@@ -1261,7 +1292,7 @@ func (vm *VM) AttestEpochBeacon(
var commitRoot [32]byte
copy(commitRoot[:], h2.Sum(nil))
return vm.attest(ctx, requestingChain, keyID, DomainEpochBeacon, subjectID, commitRoot, epoch)
return vm.attest(ctx, by, keyID, DomainEpochBeacon, subjectID, commitRoot, epoch)
}
// VerifyAttestation checks an attestation against this node's registry: the