Contract signatures

I’ve been conversing with @galfour about the topic and wanted to do a mini writeup.

There are two ways to authenticate a party in a smart-contract.

One way is to rely on a digital signature: if the party is known by its public key (or by a public key hash), then a contract may check that a message it receives has been signed by that party using the CHECK_SIGNATURE Michelson.

Another way is to authenticate parties is via the SENDER opcode which identifies the contract on the chain which sent the message. If SENDER is a tz[1…3] account, this is largely equivalent to verifying a signature. However, this method generalizes to more complex authentication method, allowing the party to be a multisig contract, a DAO, etc. This is the preferred method on Tezos.

The second method does suffer from a drawback, it ends after one contract call. If the contract being called itself calls another contract, it becomes a SENDER, and the authentication is lost.

To address this, we could allow contracts to explicitly “sign” a message by attaching their address to it during the execution of a transaction. There would be of course no actual cryptographic signature happening at all. This is merely a programming convenience that lets contracts pass between themselves messages which have been “stamped” or “signed” by some contract during the transaction.

7 Likes

Couldn’t there be cases in which i want the “effect” of the second method?
The part:

“it becomes a SENDER,”

In EVM context, there’s a msg.sender (which is the exact address that made the call) and a tx.origin (the external account whose signature is on the tx). Is the idea to allow replacement of the tx.origin with a new address, or to allow it to become a list of addresses?

1 Like

My reading is that you’ll have an instruction (e.g. STAMPED_BY) that will give you a list of all contracts in this transaction that executed a complementary instruction (STAMP).

So the list will only contain addresses that appeared as a SENDER or a subset of them, with the exception of the external address that initiated the tx ( = SOURCE, now deprecated).

My reading is something roughly like this:

  • a new type, contract_signature: allowed in parameter, not allowed in storage, not PACKable, not PUSHable
  • a new instruction, CHECK_CONTRACT_SIGNATURE :: address(?) : contract_signature : bytes : 'S -> bool : 'S
  • a new instruction, SIGN :: bytes : 'S -> contract_signature : 'S

However, for me this raises the same question I have about CHECK_SIGNATURE – how can we ensure that the message is sufficiently unambiguously associated with a meaning?

1 Like

I would suggest

  • a new type stamped_value: allowed in parameter, not allowed in storage, not packable, not pushable (agreed on that)
  • new instruction: STAMP_VALUE :: 'a : 'S -> 'a stamped_value
  • new instruction: READ_STAMPED_VALUE :: 'a stamped_value : 'S -> (pair address 'a) : 'S

(the difference with your proposal is that is being typed as opposed to using bytes and allowing to extract the address and data directly out of the signature)

The address associated with a stamped_value is always the address of the contract calling STAMP_VALUE which is what makes it a signature.

Meaning is up to the standards to make unambiguous.
This feature would considerably simplify interaction with token contracts.

Can I call directly a contract (stamped_value) or do I always need to go through another contract that calls STAMP_VALUE?

We can build replay protection in directly by making it non dupable. However, for that to be possible the read must not be destructive.

READ_STAMPED_VALUE :: 'a stamped_value : 'S -> (pair (pair address 'a) 'a stamped_value) : 'S

Not clear what you mean

I initially thought something along the line of having something like STAMPED_SENDERS, that would take care itself of replay protection and return a list of SENDERS and associated message.
Possibly by having a TRANSFER_WITH_STAMP operation.

We can build replay protection in directly by making it non dupable.

How about PAIR ; DUP ; UNPAIR ; DIP UNPAIR? Shall we add dupable as a new Michelson type attribute?

Yeah, non dupable (or rather “singleton”) would have to be yet another Michelson attribute, for some reason I thought that was already a thing.

The alternative, as you describe, doesn’t seem to attach a specific piece of data to the stamp.

Let’s assume contract A is

parameter (stamped_value nat);
storage (pair address nat);
code { UNPAIR; READ_STAMPED_VALUE; ASSERT_CMPEQ }

If the stored address is the one of an implicit account, is there a way successfully call this contract (from this address)?

We could either decide that stamping is restricted to smart contracts or allow stamping from implicit account by considering any literal of type 'a as a valid literal of type stamped_value 'a

Ah, I see what you mean. Yes, you’d be able to specify a stamped value literal in a transaction. I wouldn’t automatically convert a literal to a stamped literal, but if I sign a message from an implicit account passing a stamped literal, well that literal is clearly stamped.

I am writing a dummy prototype, where each message is associated with a string.

TRANSFER_WITH_STAMP takes bytes as an additional argument.
(Actually, a map(string,bytes), so that you can stamp multiple things. Each with a clear meaning.)

It’s not clear what you’re proposing. Is TRANSFER_WITH_STAMP the same as transfer tokens? So you’ve transferred stamped bytes, now what? Can they be retransmitted? If not, what’s the point, if yes, what’s to prevent them from being transmitted twice?

I like preventing the duping of values contained stamped_values best, but if we don’t want that complexity (something something linear type) here are two other options:

  1. Add an existing RIP instruction which “rips” a stamped value. The stamped value can still be passed around and everything, but it can only be ripped once. RIP could either FAIL if called on an already ripped value, or perhaps return a boolean: true the first time, and false thereafter. During the execution of a transaction, a global map of all stamped_value IDs is kept by the interpreter, across different invocations, to keep track of which ones have been ripped and which ones haven’t. To be clear, this isn’t a Michelson map, it’s a global value maintained by the interpreter for that transaction.

  2. Put the onus of enforcing non replayability on the contract receiving the stamped value by having it maintain a counter.

I’m not crazy about 1 because it creates a shared global state between different contracts as they get executed. I can’t think of anything bad that would happen, but it’s not good practice to do such things.

I’m not crazy about 2 because it would hand developer a powerful footgun.

Suppose I have a stamped_value (pair (address %from) (address %to)).

Can I CAST it to a stamped_value (pair address address), and then to a stamped_value (pair (address %to) (address %from)) (or whatever)?

Or, maybe, the definition of “compatibility” of types (as used in CAST and elsewhere) will have a special case for stamped_value, enforcing a stricter notion of compatibility for the stamped type?

I’m less familiar with how annotations are used inside Michelson, and I know that they have evolved in importance. If I had to pick one, I would not let this be compatible, but I don’t think it’s a big issue if it is compatible.

I suspect you’re worried about confused deputy attacks but I think that, in general, the address of the intended recipient would almost always be a part of the stamped value.

François, the SmartPy creator pointed out that “non dupable” would not allow these to be extracted from pairs. This probably isn’t an issue if we have a native UNPAIR instruction.

He proposes, alternatively, that the contract simply fails if multiple copies of a stamped_value are present in the list of passed operations.

He also suggests that not building in replay protection in stamped_value and promoting good patterns for their consumption would work as well (and let stamped_value be storable).

Just spent a few hours prototyping, doesn’t build yet, but it is available here.
The current choices I have to make:

1. There is no concept of “type of values that can be passed around in parameters, but can be created only by smart-contracts” in Michelson. Without one, users could build fake stamps (because a user can create all the values that a smart-contract can). So, we would need to add one.
This can be avoided if stamps are not passed around, but only references to stamps, stored in some other place.
So I need to make a choice there. Both make Michelson more complex.

2. There is no concept of “local effects” in Michelson. Effects in Michelson are kind-of opaque, because they can only affect context, a catchall value for all effectful functions in Tezos.
Concretely, step in Michelson has this type: step : type before after. context -> (before, after) code -> b stack -> (a stack * context). context has for instance a variable holding the remaining gas.
I would like to avoid adding more to this. So, I have another to choice to make:
- Working on adding some auxiliary type capturing local effects
- Using some other solution like the free monadic interpreter, being worked on at NL


Suggestions are welcome!