131 lines
3.5 KiB
Go
131 lines
3.5 KiB
Go
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")
|
|
}
|