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