Files
opentimestamps/cmd/ots/verify.go
2025-04-11 13:31:02 +02:00

156 lines
4.4 KiB
Go

package main
import (
"crypto/sha256"
"log/slog"
"os"
"time"
"git.intruders.space/public/opentimestamps"
"git.intruders.space/public/opentimestamps/verifyer"
"github.com/btcsuite/btcd/rpcclient"
"github.com/spf13/cobra"
)
var (
// Verify command flags
verifyEsploraURL string
verifyBitcoindHost string
verifyBitcoindUser string
verifyBitcoindPass string
verifyTimeout time.Duration
)
// verifyCmd represents the verify command
var verifyCmd = &cobra.Command{
Use: "verify [flags] <file> <file.ots>",
Short: "Verify a timestamp",
Long: `Verify a timestamp against the Bitcoin blockchain.
It computes the file digest, checks it against the timestamp,
and verifies the timestamp against Bitcoin using either Esplora API or bitcoind.`,
Args: cobra.ExactArgs(2),
Run: runVerifyCmd,
}
func init() {
// Local flags for the verify command
verifyCmd.Flags().StringVar(&verifyEsploraURL, "esplora", "https://blockstream.info/api", "URL of Esplora API")
verifyCmd.Flags().StringVar(&verifyBitcoindHost, "bitcoind", "", "Host:port of bitcoind RPC (e.g. localhost:8332)")
verifyCmd.Flags().StringVar(&verifyBitcoindUser, "rpcuser", "", "Bitcoin RPC username")
verifyCmd.Flags().StringVar(&verifyBitcoindPass, "rpcpass", "", "Bitcoin RPC password")
verifyCmd.Flags().DurationVar(&verifyTimeout, "timeout", 30*time.Second, "Connection timeout")
}
func runVerifyCmd(cmd *cobra.Command, args []string) {
filePath := args[0]
otsPath := args[1]
// Read the original file
fileData, err := os.ReadFile(filePath)
if err != nil {
slog.Error("Failed to read file", "file", filePath, "error", err)
os.Exit(1)
}
// Compute the file digest
digest := sha256.Sum256(fileData)
slog.Debug("Computed file digest", "digest", digest)
// Read and parse the OTS file
otsData, err := os.ReadFile(otsPath)
if err != nil {
slog.Error("Failed to read OTS file", "file", otsPath, "error", err)
os.Exit(1)
}
timestampFile, err := opentimestamps.ReadFromFile(otsData)
if err != nil {
slog.Error("Failed to parse OTS file", "file", otsPath, "error", err)
os.Exit(1)
}
// Setup Bitcoin connection
var bitcoin verifyer.Bitcoin
if verifyBitcoindHost != "" {
if verifyBitcoindUser == "" || verifyBitcoindPass == "" {
slog.Error("Bitcoind RPC credentials required with --bitcoind")
os.Exit(1)
}
// Create bitcoind connection config
config := rpcclient.ConnConfig{
Host: verifyBitcoindHost,
User: verifyBitcoindUser,
Pass: verifyBitcoindPass,
HTTPPostMode: true,
DisableTLS: true,
}
btc, err := verifyer.NewBitcoindInterface(config)
if err != nil {
slog.Error("Failed to connect to bitcoind", "error", err)
os.Exit(1)
}
bitcoin = btc
slog.Info("Using bitcoind", "host", verifyBitcoindHost)
} else {
// Use Esplora API
bitcoin = verifyer.NewEsploraClient(verifyEsploraURL, verifyTimeout)
slog.Info("Using Esplora API", "url", verifyEsploraURL)
}
// Check if digests match
if string(digest[:]) != string(timestampFile.Digest) {
slog.Warn("File digest doesn't match timestamp digest",
"file_digest", digest,
"timestamp_digest", timestampFile.Digest)
}
// Get Bitcoin-attested sequences
bitcoinSequences := timestampFile.GetBitcoinAttestedSequences()
if len(bitcoinSequences) == 0 {
// If no Bitcoin sequences, check if there are pending sequences
pendingSequences := timestampFile.GetPendingSequences()
if len(pendingSequences) > 0 {
slog.Info("Timestamp is pending confirmation in the Bitcoin blockchain")
slog.Info("Use 'ots upgrade <file.ots>' to try upgrading it")
for _, seq := range pendingSequences {
att := seq.GetAttestation()
slog.Info("Pending at calendar", "url", att.CalendarServerURL)
}
} else {
slog.Error("No valid attestations found in this timestamp")
}
os.Exit(1)
}
// Verify each Bitcoin-attested sequence
verificationSuccess := false
for i, seq := range bitcoinSequences {
slog.Info("Verifying sequence", "index", i+1, "total", len(bitcoinSequences))
tx, err := seq.Verify(bitcoin, timestampFile.Digest)
if err != nil {
slog.Warn("Verification failed", "error", err)
continue
}
att := seq.GetAttestation()
slog.Info("Verification successful",
"block_height", att.BitcoinBlockHeight)
if tx != nil {
slog.Info("Bitcoin transaction", "txid", tx.TxHash().String())
}
verificationSuccess = true
}
if !verificationSuccess {
slog.Error("All verification attempts failed")
os.Exit(1)
}
slog.Info("Timestamp successfully verified")
}