Skip to content

Instantly share code, notes, and snippets.

@0xekez
Last active February 6, 2023 21:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save 0xekez/98cf6abe3705f4ff83920b7d82c7f332 to your computer and use it in GitHub Desktop.
Save 0xekez/98cf6abe3705f4ff83920b7d82c7f332 to your computer and use it in GitHub Desktop.

Gas Free CosmWasm

Here we describe a simple system for interacting with CosmWasm contracts without paying gas. Instead of submitting transactions to a RPC node, addresses submit a signed ExecuteMsg to a third party and that party relays those messages to the appropriate smart contract. This third party may censor but not forge messages, and messages may be submitted as regular transactions to circumvent any censorship by the third party.

The System

Messages have the format:

{
	"payload": {
		"nonce": u64,
		"msg": ExecuteMsg,
		"expiration": Timestamp | null,
		"bech32_prefix": String,
		"to": Addr,
		"version": String
	},
	"signature": Binary,
	"pk": secp256,
}

To accept one of these messages from a smart contract:

  1. Validate that the payload is validly signed, or error.
  2. Validate that the payload has the correct nonce, or error.
  3. Validate that the payload has not expired, or error.
  4. Validate that the to field corresponds with the current contract.
  5. Validate that the version field corresponds to the verifier version.
  6. Set the message sender to the address corresponding to the provided public key.
  7. Call back into the contract's execute handler with this new sender and message.
let nonces: Map<String, u64> = Map::new("nonces");
let nonce = nonces.load(deps.storage, msg.pk)?;
deps.api.secp256k1_verify(msg.payload, msg.signature, msg.pk)?;
if msg.payload.nonce != nonce {
    return Err(NogasError::InvalidNonce)
}
if msg.payload.expiration.is_expired(&env.block) {
    return Err(NogasError::ExpiredPayload)
}
if msg.payload.to != env.contract.address {
	return Err(NoGasError::WrongReceiver)
}
if msg.payload.version != VERIFIER_VERSION {
	return Err(NoGasError::WrongVersion)
}
nonces.save(deps.storage, msg.pk, nonce + 1)?;

// call back into execute
info.sender = pk_to_addr(msg.pk)?;
execute(deps, info, env, msg.payload.msg)

This is permissionless in that any address may submit these signed messages.

Execution

image

The simplest design has a single web2 server with a private key submit messages on behalf of users. This server would likely be run by the developers. We are exploring designs for decentralizing execution:

We expect that executors will want to filter messages. For example, DAO DAO may start by only making votes free. This filtering should only happen on the executor, and not in contracts.

Conclusion

This system provides the same security and availability as an RPC node. The web2 server committing your signed messages to the chain may censor you, like an RPC node. No party can execute messages on your behalf so long so your private key is kept private.

@blue-note
Copy link

this is really interesting! and would certainly be an adoption improvement. can you please explain what attack vector the signature verification step is preventing?

@0xekez
Copy link
Author

0xekez commented Jan 18, 2023

@blue-note: can you please explain what attack vector the signature verification step is preventing?

I think it's helpful to consider the alternative where a centralized DAO DAO server takes API requests and turns them into on-chain messages.

image

Under this regime, the verification logic becomes:

if info.sender == "DAO DAO" {
  info.sender = msg.sender;
  execute(deps, info, env, msg.msg)
} else {
  Err(ContractError::Unauthorized {})
}

If the DAO DAO server is compromised, or we act maliciously, it can forge votes!

This method has users sign their messages with the same private key they would use to execute a transaction which means that while we can censor messages, we can not forge them (assuming the key is kept private).

@blue-note
Copy link

ahh, I see. I was mistakenly thinking that the verification logic would be running on the intermediary server. So the contract would verify that this message signature corresponds to the incoming public key. but then does that mean that the non-forgeability depends on the authorization logic after this line?

info.sender = pk_to_addr(msg.pk)?;

ie, what if a compromised DAO DAO server changes the message and resigns it with a different private key, and the address that corresponds to this bad key is a valid address in the contract's authorization checks. Would this be a valid forge?

@0xekez
Copy link
Author

0xekez commented Jan 18, 2023

but then does that mean that the non-forgeability depends on the authorization logic after this line?

@blue-note good point. the server could forge votes for keys that it generates, i would argue that this isn't a security concern, as anyone can do that right now by generating a new public key.

for the address derivation, i think we can do this safely. this is done in a variety of places and @NoahSaso already does it in pfpk:

https://github.com/DA0-DA0/pfpk/blob/59539c5467c93401fae62bd46a4a77720900175b/src/utils.ts#L7-L29

thanks for asking these questions btw. gets out the things that i forget to write down.

@blue-note
Copy link

Attack vectors and message manipulation

It is possible that a user may submit a message to the queue, then resubmit the same message, bypassing the queue. However, this scenario does not differ from what is possible in the current system, and in order to lower the likelihood of this occurring, we should make sure that the executor executes messages on a frequent enough cadence. We can also explore having state in the frontend which keeps track of whether a given message has been submitted to the queue, rather than if it has been executed (for example, if someone sends a message to the queue to submit a proposal, while we are waiting for the message to get executed, the frontend can display something such that a user does not mistakenly think their proposal was not submitted and attempt to re-submit).

This re-submit problem poses an issue for operations which are not idempotent, such as something which is flipping a toggle or incrementing a count.

In general, we should strive to have the behavior of the executor not differ from the current guarantees of an RPC node.

Future extensions of this

We discussed having the ability to allow DAOs to pay for the gas of its participants. One way this could be done is with the fee grant module.

Signing a list of messages in order to preserve message ordering

To be honest, this solution still does not make sense to me. The user is usually submitting one message at a time, so if we wanted to preserve the order of messages as they are coming in from the frontend, we’d have to sign the messages on the executor, but it is not clear how this would provide safety guarantees as the executor could have shuffled the ordering of messages before signing them.

@bekauz
Copy link

bekauz commented Feb 3, 2023

As far as the public key goes, are we going to be passing a compressed on uncompressed one?

@0xekez
Copy link
Author

0xekez commented Feb 6, 2023

As far as the public key goes, are we going to be passing a compressed on uncompressed one?

discussed in call: it seems like supporting both compressed and uncompressed is the same amount of work. we'll support both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment