Trustworthiness of time measurements in smart contracts

I’ve been working on a smart contract that will allow an action to occur if a minimum amount of time has passed. To do this I take an initial UTC UNIX timestamp and compare it against another one, generated when making the test. These are captured like so:

    fn current_timestamp() -> u64 {
        let now = SystemTime::now();
        let since = now.duration_since(UNIX_EPOCH);
        since
            .expect("System time is from before the UNIX Epoch. Shenanigans!")
            .as_secs()
    }

Where SystemTime is std::time::SystemTime. It occurs to me that the system time of a validator could be incorrect, intentionally or unintentionally. I’m curious about the following:

  1. How much certainty do we have about timestamps in Ootle contracts?
  2. What known attack vectors could be used here? Could a malicious validator manipulate time settings to force a contract to perform some action, without another system verifying it has been truthful?
  3. Is this the preferred way to handle time-based checks? If not, what is?
  4. Is there a known best practice for how to write unit tests for time-based actions in a smart contract?

Thanks in advance!

1 Like

I can only answer part 3 of your question. Timelocks would be the preferred way to handle this, in my opinion.
I don’t think you can use a traditional block.timestamp like you would on ETH but you should be able to use the epoch in place of a block height for the Ootle.

1 Like

Timestamps are tricky, as you say the pc timestamps can be different. This is why for most time locks, the chain and ootle uses block height as time estimations. The epochs are linked to tari L1 blocks, and you can use them probably as a timestamp and time estimate.

In terms of timestamps you can play many games with setting them lower and faster etc. But from a validator standpoint, you can probably use some margin, and they should be roughly the same.

1 Like

How can I access this from within the contract? Is there documentation/an example on how to do this? If not, is there a place in the code you can point me to where the value is available?

You can’t do time, at least not in the normal way. If you ran that code in a wasm template, you should see “time not implemented on this platform”. This makes sense, for the reasons you pointed out. Execution has to be deterministic and time is straight up not implemented for WASM. In a browser context, there is a host function provided to return time in the standard WASI api, but this is orthogonal to WASM itself.

We currently have a very limited concept of time on the ootle, for various reasons:

  • We have the epoch (roughly ~20 minute increments depending on L1 block height progress)
  • Because we have implemented sharding (even if we don’t and can’t really use it at the moment), all nodes explicitly agree on this epoch at the consensus level to allow for determinism between shards (we avoid a possible race condition across committees)
  • Block times are not “synchonised” across shards, meaning we cannot feed in that block time to the executor.

Possible solutions:

  • If acceptable for your use case, you could note the current epoch and only do the action on subsequent epochs (Consensus::current_epoch() in template)
  • The client can feed in a timestamp into the transaction itself (obviously gameable for most use cases)
  • A time oracle component - some trusted authority pushes strictly monotonic timestamp update transactions periodically that other templates use as a time oracle.
  • A VFD (verifiable delay function) - Proof of time, similar to PoW except not parallelizable. Some puzzle at some difficulty which should take X amount of time to compute. The client computes the answer and submits it to the template for verification.
  • Something else I may be missing?

I’d be interested in use cases that hit into this kind of fundemental limitation.

2 Likes

Thanks for the details here-- this is very helpful!

Surprisingly, I don’t see that error when running tests which invoke that code. I’m assuming that the template tests do the WASM compilation, and so it would be in the same WASM environment there, yes?

A ~20 minute error bar works fine for my needs.

This returns a u64– am I to assume this is a UNIX timestamp value?

In this case, we’re dealing with an escrow-style settlement. The seller puts forth that they have completed their end of the bargain (delivering, electronically or physically, whatever good or service they’ve advertised) and then a timestamp is recorded. The time window for dispute can be set, but in all real-world cases I’d expect errors of ~20-40 minutes to be negligible-- we’d be talking something like ‘two weeks to complain’ here.

If the timestamp plus the number of seconds has passed (that is, the current epoch is beyond the value that would be the initial epoch plus two weeks of seconds) then the seller can finalize the transaction and claim whatever was held by the contract in escrow.

In this case I think the epoch fuzziness is a limitation I can deal with. :slight_smile:

Happy to help!

Yeah compiled as part of the stdlib rust/library/std/src/sys/time/unsupported.rs at 06293ff2b120aecfc29f84b90a22a743a5b90fef · rust-lang/rust · GitHub

The tests do execute the wasm with the same executor as the validators. Are you sure that you’re hitting SystemTime::now() in your tests? Otherwise, what is the behaviour?

It’s an epoch number. So for Esmeralda, l1_block_height / 80 .

Double-checking, it looks I hadn’t yet written tests for those new branches, so no, I don’t have confirmation that it runs. I was about to test it manually just to make sure either way when, after removing a no-longer-needed dependency, the other ones updated and started causing compilation errors like:

error[E0277]: the trait bound `tari_template_lib::prelude::RistrettoPublicKeyBytes: serde::Deserialize<'de>` is not satisfied

This error is surprising to me-- this used to serialize, and I’d expect all the Ootle primitives to do so, so I’m planning to spend a bit more time fiddling to see if I can figure out the cause but I may need to file a bug on it. Anyhow, the current in-progress code is here, in case that error surprises you as much as it does me.

@animalst I got your update on my GitHub issue from a similar previous compilation failure and that was able to unblock me. Once unblocked, I can confirm that SystemTime panics like so:

Transaction failed: Execution failure: At instruction #5: Panic! time not implemented on this platform

So you were correct. The implementation has been replaced with one that uses the epochs instead. I’d like to test this implementation now, and I think I’ve found the method for doing so. However right next to that method is one that gives me pause. Am I to expect that my contract will sometimes have no idea what the epoch is? How might that occur?

No, this is just a test harness feature to allow certain tests on the engine (though, it’s used on one kind of stupid test, my bad AI slop slipped through). For all live executions, you will always have an epoch.

1 Like