refactor
This commit is contained in:
158
cmd/example/main.go
Normal file
158
cmd/example/main.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.intruders.space/public/opentimestamps"
|
||||
"git.intruders.space/public/opentimestamps/ots"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Read a file to timestamp
|
||||
fileData, err := os.ReadFile("document.txt")
|
||||
if err != nil {
|
||||
fmt.Println("Error reading file:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate the digest
|
||||
digest := sha256.Sum256(fileData)
|
||||
fmt.Printf("File digest: %x\n", digest)
|
||||
|
||||
// Define calendar servers
|
||||
calendars := []string{
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
}
|
||||
|
||||
// Create a timestamp using each calendar
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var sequences []ots.Sequence
|
||||
for _, calendarURL := range calendars {
|
||||
fmt.Printf("Submitting to %s...\n", calendarURL)
|
||||
seq, err := opentimestamps.Stamp(ctx, calendarURL, digest)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to submit to %s: %v\n", calendarURL, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("Submission to %s successful\n", calendarURL)
|
||||
sequences = append(sequences, seq)
|
||||
}
|
||||
|
||||
if len(sequences) == 0 {
|
||||
fmt.Println("Failed to create any timestamps")
|
||||
return
|
||||
}
|
||||
|
||||
// Create the timestamp file
|
||||
file := &ots.File{
|
||||
Digest: digest[:],
|
||||
Sequences: sequences,
|
||||
}
|
||||
|
||||
// Save the OTS file
|
||||
otsData := file.SerializeToFile()
|
||||
if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil {
|
||||
fmt.Println("Failed to save OTS file:", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("Timestamp file created successfully")
|
||||
|
||||
// Display initial timestamp info
|
||||
fmt.Println("\nInitial timestamp info:")
|
||||
fmt.Println(file.Human(false))
|
||||
|
||||
// Attempt to upgrade the timestamp every 20 minutes
|
||||
fmt.Println("\nWill check for upgrades every 20 minutes...")
|
||||
|
||||
maxAttempts := 12 // Try for about 4 hours (12 * 20 minutes)
|
||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
fmt.Printf("\nWaiting 20 minutes before next upgrade attempt (%d/%d)...\n", attempt+1, maxAttempts)
|
||||
time.Sleep(20 * time.Minute)
|
||||
}
|
||||
|
||||
upgraded := false
|
||||
pendingSequences := file.GetPendingSequences()
|
||||
if len(pendingSequences) == 0 {
|
||||
fmt.Println("No pending sequences to upgrade")
|
||||
break
|
||||
}
|
||||
|
||||
fmt.Printf("Attempting to upgrade %d pending sequences...\n", len(pendingSequences))
|
||||
|
||||
upgradeCtx, upgradeCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
for _, seq := range pendingSequences {
|
||||
att := seq.GetAttestation()
|
||||
fmt.Printf("Trying to upgrade sequence from %s...\n", att.CalendarServerURL)
|
||||
|
||||
upgradedSeq, err := opentimestamps.UpgradeSequence(upgradeCtx, seq, digest[:])
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to upgrade sequence from %s: %v\n", att.CalendarServerURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace the sequence in the file
|
||||
for i, origSeq := range file.Sequences {
|
||||
origAtt := origSeq.GetAttestation()
|
||||
if origAtt.CalendarServerURL == att.CalendarServerURL {
|
||||
file.Sequences[i] = upgradedSeq
|
||||
upgraded = true
|
||||
|
||||
newAtt := upgradedSeq.GetAttestation()
|
||||
if newAtt.BitcoinBlockHeight > 0 {
|
||||
fmt.Printf("Sequence upgraded! Confirmed in Bitcoin block %d\n", newAtt.BitcoinBlockHeight)
|
||||
} else {
|
||||
fmt.Println("Sequence updated but still pending")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
upgradeCancel()
|
||||
|
||||
if upgraded {
|
||||
// Save the upgraded file
|
||||
otsData = file.SerializeToFile()
|
||||
if err := os.WriteFile("document.txt.ots", otsData, 0644); err != nil {
|
||||
fmt.Println("Failed to save upgraded OTS file:", err)
|
||||
} else {
|
||||
fmt.Println("Upgraded timestamp file saved")
|
||||
}
|
||||
}
|
||||
|
||||
// If all sequences are confirmed, we're done
|
||||
if len(file.GetPendingSequences()) == 0 {
|
||||
fmt.Println("All sequences are now confirmed in the Bitcoin blockchain!")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Final report
|
||||
fmt.Println("\nFinal timestamp status:")
|
||||
|
||||
confirmedSeqs := file.GetBitcoinAttestedSequences()
|
||||
pendingSeqs := file.GetPendingSequences()
|
||||
|
||||
fmt.Printf("Confirmed attestations: %d\n", len(confirmedSeqs))
|
||||
for _, seq := range confirmedSeqs {
|
||||
att := seq.GetAttestation()
|
||||
fmt.Printf("- Confirmed in Bitcoin block %d\n", att.BitcoinBlockHeight)
|
||||
}
|
||||
|
||||
fmt.Printf("Pending attestations: %d\n", len(pendingSeqs))
|
||||
for _, seq := range pendingSeqs {
|
||||
att := seq.GetAttestation()
|
||||
fmt.Printf("- Still pending at %s\n", att.CalendarServerURL)
|
||||
}
|
||||
|
||||
fmt.Println("\nDetailed timestamp info:")
|
||||
fmt.Println(file.Human(true))
|
||||
}
|
||||
130
cmd/ots/create.go
Normal file
130
cmd/ots/create.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.intruders.space/public/opentimestamps"
|
||||
"git.intruders.space/public/opentimestamps/ots"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Default calendar servers
|
||||
var defaultCalendars = []string{
|
||||
"https://alice.btc.calendar.opentimestamps.org",
|
||||
"https://bob.btc.calendar.opentimestamps.org",
|
||||
"https://finney.calendar.eternitywall.com",
|
||||
}
|
||||
|
||||
var (
|
||||
// Create command flags
|
||||
createOutput string
|
||||
createCalendarsStr string
|
||||
createTimeout time.Duration
|
||||
)
|
||||
|
||||
// createCmd represents the create command
|
||||
var createCmd = &cobra.Command{
|
||||
Use: "create [flags] <file>",
|
||||
Short: "Create a timestamp for a file",
|
||||
Long: `Create a timestamp for a file by submitting its digest to OpenTimestamps calendar servers.
|
||||
The resulting timestamp is saved to a .ots file, which can later be verified or upgraded.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runCreateCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Local flags for the create command
|
||||
createCmd.Flags().StringVarP(&createOutput, "output", "o", "", "Output filename (default: input filename with .ots extension)")
|
||||
createCmd.Flags().StringVar(&createCalendarsStr, "calendar", strings.Join(defaultCalendars, ","), "Comma-separated list of calendar server URLs")
|
||||
createCmd.Flags().DurationVar(&createTimeout, "timeout", 30*time.Second, "Timeout for calendar server connections")
|
||||
}
|
||||
|
||||
func runCreateCmd(cmd *cobra.Command, args []string) {
|
||||
inputPath := args[0]
|
||||
|
||||
// Determine output file path
|
||||
outputPath := createOutput
|
||||
if outputPath == "" {
|
||||
outputPath = inputPath + ".ots"
|
||||
}
|
||||
|
||||
// Parse calendar servers
|
||||
calendars := strings.Split(createCalendarsStr, ",")
|
||||
if len(calendars) == 0 {
|
||||
calendars = defaultCalendars
|
||||
}
|
||||
|
||||
// Read the input file
|
||||
fileData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to read input file", "file", inputPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Compute the file digest
|
||||
digest := sha256.Sum256(fileData)
|
||||
slog.Info("Computed file digest", "digest", fmt.Sprintf("%x", digest))
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), createTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Create timestamps using all calendar servers
|
||||
var sequences []ots.Sequence
|
||||
var errors []string
|
||||
|
||||
for _, calendarURL := range calendars {
|
||||
calendarURL = strings.TrimSpace(calendarURL)
|
||||
if calendarURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("Submitting to calendar", "url", calendarURL)
|
||||
seq, err := opentimestamps.Stamp(ctx, calendarURL, digest)
|
||||
if err != nil {
|
||||
slog.Warn("Calendar submission failed", "url", calendarURL, "error", err)
|
||||
errors = append(errors, fmt.Sprintf("%s: %v", calendarURL, err))
|
||||
continue
|
||||
}
|
||||
|
||||
sequences = append(sequences, seq)
|
||||
slog.Info("Calendar submission successful", "url", calendarURL)
|
||||
}
|
||||
|
||||
if len(sequences) == 0 {
|
||||
slog.Error("All calendar submissions failed", "errors", errors)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create the timestamp file
|
||||
file := &ots.File{
|
||||
Digest: digest[:],
|
||||
Sequences: sequences,
|
||||
}
|
||||
|
||||
// Write the OTS file
|
||||
otsData := file.SerializeToFile()
|
||||
err = os.WriteFile(outputPath, otsData, 0644)
|
||||
if err != nil {
|
||||
slog.Error("Failed to write OTS file", "file", outputPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Timestamp file created successfully",
|
||||
"file", outputPath,
|
||||
"timestamps", len(sequences),
|
||||
"size", len(otsData))
|
||||
|
||||
// Print human-readable representation
|
||||
slog.Debug("Timestamp details", "info", file.Human(false))
|
||||
|
||||
slog.Info("Timestamp creation complete",
|
||||
"status", "pending",
|
||||
"note", "Use 'ots upgrade' to upgrade this timestamp when it's confirmed in the Bitcoin blockchain")
|
||||
}
|
||||
108
cmd/ots/info.go
Normal file
108
cmd/ots/info.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.intruders.space/public/opentimestamps"
|
||||
"git.intruders.space/public/opentimestamps/ots"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// infoCmd represents the info command
|
||||
var infoCmd = &cobra.Command{
|
||||
Use: "info [flags] <file.ots>",
|
||||
Short: "Display information about a timestamp",
|
||||
Long: `Display detailed information about a timestamp file in human-readable format.
|
||||
Shows the file digest and the sequence of operations and attestations.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runInfoCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// No specific flags needed for info command
|
||||
}
|
||||
|
||||
func runInfoCmd(cmd *cobra.Command, args []string) {
|
||||
otsPath := args[0]
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Print the timestamp information in the requested format
|
||||
fmt.Printf("File sha256 hash: %s\n", hex.EncodeToString(timestampFile.Digest))
|
||||
fmt.Println("Timestamp:")
|
||||
|
||||
// Format and print each sequence
|
||||
for i, seq := range timestampFile.Sequences {
|
||||
if i > 0 {
|
||||
// Add a separator between sequences if needed
|
||||
fmt.Println()
|
||||
}
|
||||
printSequenceInfo(seq, 0, timestampFile.Digest)
|
||||
}
|
||||
}
|
||||
|
||||
func printSequenceInfo(seq ots.Sequence, depth int, initialDigest []byte) {
|
||||
prefix := strings.Repeat(" ", depth)
|
||||
|
||||
// Track the current result as we apply operations
|
||||
current := initialDigest
|
||||
|
||||
// For the first level (depth 0), don't add the arrow
|
||||
arrowPrefix := ""
|
||||
if depth > 0 {
|
||||
arrowPrefix = " -> "
|
||||
}
|
||||
|
||||
for _, inst := range seq {
|
||||
// Skip attestation for now, we'll handle it at the end
|
||||
if inst.Attestation != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Print operation
|
||||
if inst.Operation != nil {
|
||||
line := fmt.Sprintf("%s%s%s", prefix, arrowPrefix, inst.Operation.Name)
|
||||
if inst.Operation.Binary {
|
||||
line += fmt.Sprintf(" %s", hex.EncodeToString(inst.Argument))
|
||||
}
|
||||
fmt.Println(line)
|
||||
|
||||
// Update current result
|
||||
current = inst.Operation.Apply(current, inst.Argument)
|
||||
|
||||
// Only show arrow prefix for the first line
|
||||
arrowPrefix = ""
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there's an attestation at the end
|
||||
if len(seq) > 0 && seq[len(seq)-1].Attestation != nil {
|
||||
att := seq[len(seq)-1].Attestation
|
||||
var attLine string
|
||||
|
||||
if att.BitcoinBlockHeight > 0 {
|
||||
attLine = fmt.Sprintf("verify BitcoinAttestation(block %d)", att.BitcoinBlockHeight)
|
||||
} else if att.CalendarServerURL != "" {
|
||||
attLine = fmt.Sprintf("verify PendingAttestation('%s')", att.CalendarServerURL)
|
||||
}
|
||||
|
||||
if attLine != "" {
|
||||
fmt.Printf("%s%s\n", prefix, attLine)
|
||||
}
|
||||
}
|
||||
}
|
||||
13
cmd/ots/main.go
Normal file
13
cmd/ots/main.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
64
cmd/ots/root.go
Normal file
64
cmd/ots/root.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Global flags
|
||||
jsonLog bool
|
||||
logLevel string
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ots",
|
||||
Short: "OpenTimestamps CLI tool",
|
||||
Long: `A command-line interface for OpenTimestamps operations.
|
||||
It allows creating, verifying and upgrading timestamps for files.`,
|
||||
PersistentPreRun: setupLogging,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Global flags for all commands
|
||||
rootCmd.PersistentFlags().BoolVar(&jsonLog, "json", false, "Use JSON format for logging")
|
||||
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Log level: debug, info, warn, error")
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(createCmd)
|
||||
rootCmd.AddCommand(verifyCmd)
|
||||
rootCmd.AddCommand(upgradeCmd)
|
||||
rootCmd.AddCommand(infoCmd)
|
||||
}
|
||||
|
||||
// setupLogging configures the global logger based on the flags
|
||||
func setupLogging(cmd *cobra.Command, args []string) {
|
||||
loggerOptions := slog.HandlerOptions{}
|
||||
|
||||
switch strings.ToLower(logLevel) {
|
||||
case "debug":
|
||||
loggerOptions.Level = slog.LevelDebug
|
||||
case "info":
|
||||
loggerOptions.Level = slog.LevelInfo
|
||||
case "warn":
|
||||
loggerOptions.Level = slog.LevelWarn
|
||||
case "error":
|
||||
loggerOptions.Level = slog.LevelError
|
||||
default:
|
||||
loggerOptions.Level = slog.LevelInfo
|
||||
}
|
||||
|
||||
var logHandler slog.Handler
|
||||
if jsonLog {
|
||||
logHandler = slog.NewJSONHandler(os.Stdout, &loggerOptions)
|
||||
} else {
|
||||
logHandler = slog.NewTextHandler(os.Stdout, &loggerOptions)
|
||||
}
|
||||
|
||||
logger := slog.New(logHandler)
|
||||
slog.SetDefault(logger)
|
||||
}
|
||||
138
cmd/ots/upgrade.go
Normal file
138
cmd/ots/upgrade.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.intruders.space/public/opentimestamps"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// Upgrade command flags
|
||||
upgradeOutput string
|
||||
upgradeTimeout time.Duration
|
||||
upgradeDryRun bool
|
||||
)
|
||||
|
||||
// upgradeCmd represents the upgrade command
|
||||
var upgradeCmd = &cobra.Command{
|
||||
Use: "upgrade [flags] <file.ots>",
|
||||
Short: "Upgrade a timestamp",
|
||||
Long: `Upgrade a timestamp by checking if pending attestations have been confirmed in the Bitcoin blockchain.
|
||||
If confirmed, the timestamp will be updated with the Bitcoin block information.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: runUpgradeCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Local flags for the upgrade command
|
||||
upgradeCmd.Flags().StringVarP(&upgradeOutput, "output", "o", "", "Output filename (default: overwrites the input file)")
|
||||
upgradeCmd.Flags().DurationVar(&upgradeTimeout, "timeout", 30*time.Second, "Timeout for calendar server connections")
|
||||
upgradeCmd.Flags().BoolVar(&upgradeDryRun, "dry-run", false, "Don't write output file, just check if upgrade is possible")
|
||||
}
|
||||
|
||||
func runUpgradeCmd(cmd *cobra.Command, args []string) {
|
||||
inputPath := args[0]
|
||||
|
||||
// Determine output file path
|
||||
outputPath := upgradeOutput
|
||||
if outputPath == "" {
|
||||
outputPath = inputPath
|
||||
}
|
||||
|
||||
// Read and parse the OTS file
|
||||
otsData, err := os.ReadFile(inputPath)
|
||||
if err != nil {
|
||||
slog.Error("Failed to read OTS file", "file", inputPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
timestampFile, err := opentimestamps.ReadFromFile(otsData)
|
||||
if err != nil {
|
||||
slog.Error("Failed to parse OTS file", "file", inputPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get pending sequences to upgrade
|
||||
pendingSequences := timestampFile.GetPendingSequences()
|
||||
if len(pendingSequences) == 0 {
|
||||
slog.Info("No pending timestamps found, file is already fully upgraded", "file", inputPath)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
slog.Info("Found pending timestamps", "count", len(pendingSequences))
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), upgradeTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Try to upgrade each pending sequence
|
||||
upgradedCount := 0
|
||||
for i, seq := range pendingSequences {
|
||||
att := seq.GetAttestation()
|
||||
slog.Info("Attempting to upgrade timestamp",
|
||||
"index", i+1,
|
||||
"calendar", att.CalendarServerURL)
|
||||
|
||||
upgraded, err := opentimestamps.UpgradeSequence(ctx, seq, timestampFile.Digest)
|
||||
if err != nil {
|
||||
slog.Warn("Upgrade failed",
|
||||
"calendar", att.CalendarServerURL,
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace the pending sequence with the upgraded one
|
||||
for j, origSeq := range timestampFile.Sequences {
|
||||
if origSeq[len(origSeq)-1].Attestation != nil &&
|
||||
origSeq[len(origSeq)-1].Attestation.CalendarServerURL == att.CalendarServerURL {
|
||||
timestampFile.Sequences[j] = upgraded
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
upgradedCount++
|
||||
|
||||
newAtt := upgraded.GetAttestation()
|
||||
if newAtt.BitcoinBlockHeight > 0 {
|
||||
slog.Info("Timestamp upgraded successfully",
|
||||
"calendar", att.CalendarServerURL,
|
||||
"block", newAtt.BitcoinBlockHeight)
|
||||
} else {
|
||||
slog.Info("Timestamp replaced but still pending", "calendar", att.CalendarServerURL)
|
||||
}
|
||||
}
|
||||
|
||||
if upgradedCount == 0 {
|
||||
slog.Warn("No timestamps could be upgraded at this time. Try again later.", "file", inputPath)
|
||||
if !upgradeDryRun {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// In dry run mode, don't write the file
|
||||
if upgradeDryRun {
|
||||
slog.Info("Dry run completed", "upgraded", upgradedCount, "total", len(pendingSequences))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Write the updated OTS file
|
||||
newOtsData := timestampFile.SerializeToFile()
|
||||
err = os.WriteFile(outputPath, newOtsData, 0644)
|
||||
if err != nil {
|
||||
slog.Error("Failed to write updated OTS file", "file", outputPath, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
slog.Info("Timestamp file upgraded successfully",
|
||||
"file", outputPath,
|
||||
"upgraded", upgradedCount,
|
||||
"total", len(pendingSequences))
|
||||
|
||||
// Print human-readable representation
|
||||
slog.Debug("Updated timestamp details", "info", timestampFile.Human(false))
|
||||
}
|
||||
155
cmd/ots/verify.go
Normal file
155
cmd/ots/verify.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user