Input Manager

The StoffelInputManager contract handles client input submission for MPC computations, including mask reservation and ECDSA authentication.

Overview

abstract contract StoffelInputManager {
    // Manages client inputs with privacy-preserving masking
}

How Input Masking Works

Clients don't submit raw inputs on-chain (that would reveal them). Instead:

  1. MPC nodes generate input masks during preprocessing
  2. Clients reserve mask indices on-chain
  3. Clients submit masked inputs: masked = input + mask
  4. MPC nodes unmask during computation using their mask shares
Client Input: 42
Mask:         17
Masked Input: 59  ← This goes on-chain (reveals nothing about 42)

Data Structures

MaskedInput

struct MaskedInput {
    uint256 index;        // Reserved mask index
    uint256 maskedInput;  // Input XOR/+ mask
}

Inputs

struct Inputs {
    bytes publicInputs;           // Optional public parameters
    MaskedInput[] maskedInputs;   // Array of masked secret inputs
}

Outputs

struct Outputs {
    bytes publicOutputs;          // Computation results
    mapping(address => mapping(address => bool)) sharesReceived;
    // Tracks: client => party => received
}

Storage

// Maps index to reserving client
mapping(uint256 => address) public reservedInputIndices;

// Total indices available
uint256 public nTotalIndices;

// Remaining unreserved indices
uint256 public nIndicesLeft;

// Client inputs storage
mapping(address => MaskedInput) public clientInputs;

Functions

initializeInputMaskBuffer

Sets up the input mask buffer. Called by designated party during preprocessing.

function initializeInputMaskBuffer(uint256 nIndicesToReserve)
    external
    onlyDesignatedParty
{
    nTotalIndices = nIndicesToReserve;
    nIndicesLeft = nIndicesToReserve;
}

reserveInputMask

Clients call this to reserve an input mask index.

function reserveInputMask(uint256 indexToReserve) external {
    require(indexToReserve < nTotalIndices, "Index out of bounds");
    require(reservedInputIndices[indexToReserve] == address(0), "Index already reserved");
    require(nIndicesLeft > 0, "No indices left");

    reservedInputIndices[indexToReserve] = msg.sender;
    nIndicesLeft--;

    emit InputMaskReserved(msg.sender, indexToReserve);
}

submitMaskedInput

Clients submit their masked input using a reserved index.

function submitMaskedInput(uint256 maskedInput, uint256 reservedIndex) external {
    require(reservedInputIndices[reservedIndex] == msg.sender, "Not your reserved index");

    clientInputs[msg.sender] = MaskedInput({
        index: reservedIndex,
        maskedInput: maskedInput
    });

    // Unreserve the index (one-time use)
    reservedInputIndices[reservedIndex] = address(0);

    emit MaskedInputSubmitted(msg.sender, reservedIndex);
}

authenticateClient

MPC nodes use this for off-chain client authentication via ECDSA.

function authenticateClient(
    uint256 requestIndex,
    address clientAddr,
    bytes calldata signature
) external view returns (bool) {
    // Construct the message hash
    bytes32 messageHash = keccak256(abi.encode(requestIndex));
    bytes32 ethSignedHash = keccak256(
        abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
    );

    // Recover signer from signature
    address recovered = recoverSigner(ethSignedHash, signature);

    return recovered == clientAddr;
}

getClientInput

Retrieve a client's submitted input.

function getClientInput(address client)
    external
    view
    returns (MaskedInput memory)
{
    return clientInputs[client];
}

hasClientSubmitted

Check if a client has submitted their input.

function hasClientSubmitted(address client) external view returns (bool) {
    return clientInputs[client].maskedInput != 0 ||
           clientInputs[client].index != 0;
}

Events

event InputMaskReserved(address indexed client, uint256 indexed index);
event MaskedInputSubmitted(address indexed client, uint256 indexed index);
event ClientAuthenticated(address indexed client, uint256 indexed requestIndex);

Client Workflow

1. Reserve a Mask Index

// Client reserves index 5
await coordinator.reserveInputMask(5);

2. Get the Mask (Off-Chain)

// Client contacts MPC nodes to get their mask
// This happens off-chain via the Rust SDK
const mask = await mpcClient.getInputMask(5);

3. Compute Masked Input

// Client masks their secret input
const secretInput = 42n;
const maskedInput = secretInput + mask;  // or XOR depending on protocol

4. Submit Masked Input

// Client submits on-chain
await coordinator.submitMaskedInput(maskedInput, 5);

5. MPC Nodes Unmask

During computation, MPC nodes:

  1. Read maskedInput from contract
  2. Subtract their mask share
  3. Proceed with MPC on the unmasked value

Authentication Flow

For off-chain operations, clients prove their identity:

// Client signs a request
const requestIndex = 12345;
const messageHash = ethers.utils.keccak256(
    ethers.utils.defaultAbiCoder.encode(['uint256'], [requestIndex])
);
const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));

// MPC node verifies on-chain
const isValid = await coordinator.authenticateClient(
    requestIndex,
    clientAddress,
    signature
);

Example: Complete Input Flow

contract SecureVoting is StoffelCoordinator {
    mapping(address => bool) public hasVoted;

    function vote(uint256 maskedVote, uint256 maskIndex) external {
        require(!hasVoted[msg.sender], "Already voted");
        require(currentRound == Round.CollectingClientInputRound, "Not collecting");

        // Verify client reserved this index
        require(reservedInputIndices[maskIndex] == msg.sender, "Wrong index");

        // Submit the masked vote
        clientInputs[msg.sender] = MaskedInput({
            index: maskIndex,
            maskedInput: maskedVote
        });

        hasVoted[msg.sender] = true;
        reservedInputIndices[maskIndex] = address(0);

        emit MaskedInputSubmitted(msg.sender, maskIndex);
    }
}

Security Considerations

Index Reservation

  • Each index can only be reserved once
  • Prevents double-spending of masks
  • Clients should reserve early to ensure availability

Mask Uniqueness

  • Each mask is used exactly once
  • After submission, the index is unreserved
  • Prevents mask reuse attacks

Authentication

  • ECDSA signatures verify client identity
  • Prevents impersonation in off-chain communications
  • Message includes unique requestIndex to prevent replay

Input Privacy

  • Only masked values appear on-chain
  • Raw inputs never touch the blockchain
  • Privacy depends on MPC node security (threshold trust)

Next Steps