roughly
An asynchronous implementation of the Roughtime protocol for Python.
Implements the experimental Roughtime protocol specified in RFC 10049.
The client and server support Roughtime version 1, with the server preferring it when offered by a client.
Drafts 07 through 19 remain supported for querying servers, and drafts 10 through 19 for running a server. The server also supports queries from Google Roughtime clients.
Quickstart
Installation
You can install roughly from PyPI using your favorite package manager, for example with pip:
pip install roughly
# or with the cli extra
pip install roughly[cli]
As a CLI
Querying
You can use roughly as a command line tool to query Roughtime servers.
Install roughly with the cli extra using your favorite CLI package manager, for example with uv (or pipx):
uv tool install roughly[cli]
pipx install roughly[cli]
Then you can query a Roughtime server like so:
roughly query time.teax.dev 2002 84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg=
The default Roughtime port is 5319. This example explicitly uses the server's configured port, 2002.
Or run ecosystem queries (assuming you have an ecosystem.json file):
roughly ecosystem malfeasance
roughly ecosystem state
Running a server
You can also run your own Roughtime server using roughly.
First, generate a keypair:
roughly server keygen
This will output a .env file containing the server's private key.
You can then run the server like so:
ROUGHLY_PRIVATE_KEY="your_private_key_here" roughly -v server run
By default, the server will bind to 0.0.0.0:5319. You can change this using the --host and --port flags.
I recommend running the server with verbose logging enabled (-v), so you can see incoming requests and debug any issues.
Additionally you might want to consider turning off response greasing while testing using the --no-grease flag.
Every server run option can also be set through an environment variable, named after
the flag with a ROUGHLY_ prefix. Check roughly server run --help for more information.
As a library
Querying
roughly can be used as an asynchronous library to query Roughtime servers from your own Python code.
import roughly.client
# <snip!>
response = await roughly.client.send_request(
host="time.teax.dev",
port=2002,
public_key=base64.b64decode(b"84pMADvKUcSOq5RNbVRjVrjiU16Dxo2XV2Qkm+4DRTg="),
)
midpoint = response.signed_response.midpoint
radius = response.signed_response.radius
print(f"time: {midpoint} ± {radius}s")
send_request verifies the response before returning. Any failure raises roughly.errors.VerificationError; malformed packets raise PacketError. Both inherit from RoughtimeError. Once you have a response, true time is somewhere in the range [midpoint - radius, midpoint + radius].
An ecosystem is a list of servers a client can query.
roughly.ecosystem provides a flow for querying them and checking for disagreement:
from pathlib import Path
import json
from roughly.ecosystem import (
confirm_malfeasance,
load_ecosystem,
malfeasance_report,
pick_servers,
query_servers,
)
ecosystem = load_ecosystem(Path("ecosystem.json"))
selected_servers = await pick_servers(ecosystem)
responses = await query_servers(selected_servers)
report = malfeasance_report(responses, selected_servers)
if confirm_malfeasance(report):
print("something scary is going on!")
with open("malfeasance_report.json", "w") as f:
json.dump(report, f, indent=2)
pick_servers filters down to servers that are actually reachable right now. query_servers returns one (VerifiableResponse, raw_bytes) per server. confirm_malfeasance returns true when the responses can't all be true at the same time.
Running a server
You can also programmatically run your own Roughtime server. Server.create() mints a long-term ed25519 keypair on each call, so for any server clients should be able to keep talking to across restarts, pass private_key=... with a key you've persisted yourself:
import roughly.server
server = roughly.server.Server.create() # generates a fresh keypair
await roughly.server.serve(server)
The reason to use the library directly instead of the CLI is that both roughly.server.Server and roughly.server.UDPHandler are designed to be extended. A sample use case is a deliberately malfeasant server:
import roughly
import roughly.server
class ScaryServer(roughly.server.Server):
@staticmethod
def get_time() -> int:
# return a wrong-ish time
return int(time.time()) + random.randint(-3600, 3600)
await roughly.server.serve(ScaryServer.create())
Ecosystem
An example ecosystem file can be found at ecosystem.json, I tried my best to include as many servers as I could find.
If you know of any other Roughtime servers, run your own server, or have updated public keys for any of the listed servers, please open a PR or an issue!
Interoperability
The interopability matrix of roughly against Roughtime servers looks like this:
Roughly as a client
| Server | Result |
|---|---|
| butterfield | ✅ |
| cloudflare | ✅ |
| pyroughtime | ✅ |
| roughenough | ✅ |
| roughtimed | ✅ |
| roughly | ✅ |
| tannerryan-roughtime | ✅ |
Roughly as a server
| Client | Result |
|---|---|
| cloudflare | ✅ |
| craggy | ✅ |
| node-roughtime | ✅ |
| pyroughtime | ✅ |
| roughenough | ✅ |
| roughly | ✅ |
| tannerryan-roughtime | ✅ |
| vroughtime | ✅ |
draft-7
Support for draft-7 is limited, in the sense that roughly fits responses from draft-7 servers into its current response data structures.
This means that some fields that are not present in draft-8+ (such as DUT1, DTAI, and LEAP) will be missing.
Additionally draft-7 offered for the precision of radius to be in microseconds, while draft-8+ uses seconds, this precision will be lost when querying draft-7 servers, and be clamped to a minimum of one second.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Metadata
Release files for roughly 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| roughly-0.3.0.tar.gz | 25.7 kB | Without provenance | Details for roughly-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| roughly-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for roughly-0.3.0-py3-none-any.whl |
Total release size: 55.1 kB
Release files / roughly-0.3.0.tar.gz
| Download URL | roughly-0.3.0.tar.gz |
|---|---|
| Size | 25.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
62ea8ab9c84aa574704ae7c5fb6d93abfbe0ced43c62374149ecc4269153a79a
|
|
BLAKE2b-256 checksum How to use checksums |
ec7e2c464e5c2738d8a3559ef5e0bd3dd7c4068baab491b45b3803d6952d9454
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / roughly-0.3.0-py3-none-any.whl
| Download URL | roughly-0.3.0-py3-none-any.whl |
|---|---|
| Size | 29.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b3e3a6bf1d327454dca80bccd4b4477d77f1efd63085cb97cc08bf4322a1e6bf
|
|
BLAKE2b-256 checksum How to use checksums |
af669f080ab73836b5605edae759b85059af705b56eb67fb7a96bdfbe0b3406e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Arch Linux","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for roughly 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.