near / mpc

41 stars 15 forks source link

Accept multiple signature requests in single transaction #880

Open volovyks opened 2 weeks ago

volovyks commented 2 weeks ago

MPC system should accept multiple payloads for signing. Obviously we can accept a longer TX time, and pay more, but it seems like most teams working with Bitcoin UTXOs are scratching their heads trying to come up with solutions for how to spend multiple UTXOs.

Each UTXO to spend requires it's own signature. So with the current configuration of MPC we can really only spend from one UTXO.

Possible solutions outside of the MPC accepting multiple payloads would be:

Seeing as how everyone is going to want to do this sooner than later, for Bitcoin UTXOs and other applications, I think it should be a priority.

(requested by @mattlockyer)

volovyks commented 2 weeks ago

@mattlockyer I agree that we should add this feature. Ideally, it should be incorporated into the contract API:

pub fn sign(&mut self, requests: Vec<SignRequest>) -> Result<near_sdk::Promise, Error>

I'm unsure whether we can make it synchronous with the current yield/resume design, but an asynchronous version should be feasible, though it may require more effort than it initially seems.

mattlockyer commented 1 week ago

@volovyks I was just experimenting calling the MPC contract with multiple functionCall actions per transaction.

It behaves a bit oddly. Basically sign_helper is called 3 times, once for each functionCall action (that call sign on MPC contract).

Then things get weird. clear_state_on_finish and return_signature_on_finish are only called once. The client only receives 1 status and SuccessValue for the transaction. Despite having 3 functionCall actions attached.

See the following result:

https://testnet.nearblocks.io/txns/6Y3sWjnrytHuTXL3ELkQkdbzbnyeBEQzJpZn2q5B4pAj#enhanced

If there is a way to process all 3 actions independently and return the array of promise results to the client, that would be ideal.

volovyks commented 1 week ago

@mattlockyer are you making a batch transaction with 3 function_call sign actions? This can be a good way to achieve the goal of this issue with less effort. This flow was never tested, it may be connected to yield/resume logic or how we assign signature request id. @ppca can you please take a look?

I will add this issue to our board for better visibility.

ppca commented 1 week ago

will take a look

ppca commented 1 week ago

@mattlockyer if you open the enhanced plan of that transaction: https://testnet.nearblocks.io/txns/6Y3sWjnrytHuTXL3ELkQkdbzbnyeBEQzJpZn2q5B4pAj#enhanced It says

{
  "type": "action",
  "error": {
    "type": "functionCallError",
    "error": {
      "type": "executionError",
      "error": "Exceeded the prepaid gas."
    }
  }

Can you try attaching more gas to it?

mattlockyer commented 1 week ago

@mattlockyer are you making a batch transaction with 3 function_call sign actions?

Yes I am, thanks for clarifying. I would be interested in this flow being adopted, since it would open a lot of use cases for handling multiple signatures. Thanks!

volovyks commented 1 week ago

@mattlockyer does it work with increased gas?

mattlockyer commented 1 week ago

@mattlockyer does it work with increased gas?

The transaction is eventually successful with gas at 95 Tgas per action (smaller gas amounts like 50 and 75 Tgas failed).

But the issue is only 1 signature is returned to the client.

mattlockyer commented 1 week ago

@mattlockyer if you open the enhanced plan of that transaction: https://testnet.nearblocks.io/txns/6Y3sWjnrytHuTXL3ELkQkdbzbnyeBEQzJpZn2q5B4pAj#enhanced It says

{
  "type": "action",
  "error": {
    "type": "functionCallError",
    "error": {
      "type": "executionError",
      "error": "Exceeded the prepaid gas."
    }
  }

Can you try attaching more gas to it?

Yes I see... This is unfortunate as I attached 95 Tgas per action.

ppca commented 1 week ago

@mattlockyer May I know how you sent the transaction? Any script or commands? So i could reproduce and look into it

mattlockyer commented 1 week ago

This is a hacky implementation of a batch action call to MPC sign with 3 actions, each slightly modifies the payload. Currently does not work with MPC contract:


// finalArgs are just your args to sign call
const finalArgs = args;
const actions = [];
for (let i = 0; i < 3; i++) {
    // DEBUGGING copy args and modify payload slightly
    const args = JSON.parse(JSON.stringify(finalArgs));
    if (i > 0) {
        args.request.payload.pop();
        args.request.payload.push(i);
    }
// import functionCall from nearApi.transactions.functionCall
    actions.push(
        functionCall(
            'sign',
            args,
            new BN('95000000000000'),
            new BN(attachedDeposit),
        ),
    );
}

let res: nearAPI.providers.FinalExecutionOutcome;
try {
    // receiverId is the NEAR MPC CONTRACT
    // account is a nearApi Account instance
    res = await account.signAndSendTransaction({
        receiverId: contractId,
        actions,
    });
} catch (e) {
    throw new Error(`error signing ${JSON.stringify(e)}`);
}

console.log('NEAR RESPONSE', res);
mattlockyer commented 6 days ago

Then things get weird. clear_state_on_finish and return_signature_on_finish are only called once. The client only receives 1 status and SuccessValue for the transaction. Despite having 3 functionCall actions attached.

I was mistaken: clear_state_on_finish and return_signature_on_finish were called but exceeded the prepaid gas.

They both had their promise yields timeout at 201 blocks after sign_helper was called at block, 176,903,682. Both timeout in the same block.

Despite the other 2 calls to sign failing for exceeding gas. The client still only received 1 signature response in the final transaction SuccessValue.

@EdsonAlcala and myself looked at the code and wondered the following:

In the transaction: https://testnet.nearblocks.io/txns/6Y3sWjnrytHuTXL3ELkQkdbzbnyeBEQzJpZn2q5B4pAj#execution

sign is called 3 times in the same block.

sign_helper is called 3 times, 2 blocks later, but all calls fall in the same block.

One of the responses is successful, and at block 176,903,489 clear_state_on_finish is called.

The other 2 calls to sign_helper that were yielded (in the same block as the successful one) time out exactly 201 blocks after sign_helper is called, block 176,903,682.

Question:

When the 3 yield promises are created, the DATA_ID_REGISTER is being written to and read back in this same block.

Is it possible that the yield promise data ID being written to and read from the register is returning the same value for all 3 calls?

This could manifest into a number of different runtime errors, but would essentially leave the other 2 calls to sign_helper yielded but never resumed (created but missing a reference?).

We're unclear on how the yield promise resume logic works, but found this portion of code to be troublesome if executed in the same block, same transaction, same predecessor.

https://github.com/near/mpc/blob/ecc6fd32ba623025963f34810dca261ec1b5f0d4/chain-signatures/contract/src/lib.rs#L682-L696

Another, possibly related issue of multiple async calls to sign_helper:

https://github.com/near/mpc/issues/901

ppca commented 2 days ago

Checking with near protocol on the workings of yield resume

ppca commented 2 days ago

Seems like there is nothing special for multiple yield resume in the same block. What happens when you tried attaching more gas? @mattlockyer

mattlockyer commented 2 days ago

I was only ever able to get the 1/3 signature result in the transaction https://testnet.nearblocks.io/txns/6Y3sWjnrytHuTXL3ELkQkdbzbnyeBEQzJpZn2q5B4pAj#execution

I used 95 Tgas per function call.

ppca commented 2 days ago

I will reproduce it on my end and I'll check our backend and see. Will let you know

ppca commented 1 day ago

I used near rust cli to send a batch transaction of 3 signs:

    /Users/xiangyiz/workspace/near/near-cli-rs/target/release/near transaction construct-transaction v5.multichain-mpc-dev.testnet v1.signer-prod.testnet add-action function-call sign json-args '{"request": {"payload":[12, 1, 2, 0, 4, 5, 6, 8, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 44], "path": "test", "key_version": 0}}' prepaid-gas '95.0 Tgas' attached-deposit '1 NEAR' add-action function-call sign json-args '{"request": {"payload":[14, 1, 2, 0, 4, 5, 6, 8, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 44], "path": "test", "key_version": 0}}' prepaid-gas '95.0 Tgas' attached-deposit '1 NEAR' add-action function-call sign json-args '{"request": {"payload":[15, 1, 2, 0, 4, 5, 6, 8, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 44], "path": "test", "key_version": 0}}' prepaid-gas '95.0 Tgas' attached-deposit '1 NEAR' skip network-config testnet sign-with-keychain send

I'm getting https://testnet.nearblocks.io/txns/EioG9uiYEe64eyMYR8KcVmcvP7TVGWNTZWHiMAANbZkc#enhanced one of the signs return signature, while the other 2 failed with request has timed out.

I have figured out why they fail with time out, and working on a fix.

I am not able to reproduce the error you had exceeded prepaid gas. @mattlockyer

ppca commented 2 hours ago

Hey I'm mostly done with the implementation just trying to make an integration test of this use case, which is taking a bit of time. ETA to dev network is next Monday or Tuesday.