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] ", 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)) }