simplify codebase.
This commit is contained in:
@@ -97,10 +97,7 @@ func (b *BitcoinAttestation) encode(ctx *serializationContext) error {
|
|||||||
|
|
||||||
const hashMerkleRootSize = 32
|
const hashMerkleRootSize = 32
|
||||||
|
|
||||||
//
|
func (b *BitcoinAttestation) VerifyAgainstBlockHash(digest, blockHash []byte) error {
|
||||||
func (b *BitcoinAttestation) VerifyAgainstBlockHash(
|
|
||||||
digest, blockHash []byte,
|
|
||||||
) error {
|
|
||||||
if len(digest) != hashMerkleRootSize {
|
if len(digest) != hashMerkleRootSize {
|
||||||
return fmt.Errorf("invalid digest size %d", len(digest))
|
return fmt.Errorf("invalid digest size %d", len(digest))
|
||||||
}
|
}
|
||||||
|
|||||||
7
bitcoind.go
Normal file
7
bitcoind.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package opentimestamps
|
||||||
|
|
||||||
|
import "github.com/btcsuite/btcd/rpcclient"
|
||||||
|
|
||||||
|
func NewBitcoindInterface(config rpcclient.ConnConfig) (Bitcoin, error) {
|
||||||
|
return rpcclient.New(&config, nil)
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/rpcclient"
|
|
||||||
"github.com/fiatjaf/opentimestamps"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const envvarRPCURL = "GOTS_TEST_BITCOIN_RPC"
|
|
||||||
|
|
||||||
func newTestBTCConn() (*rpcclient.Client, error) {
|
|
||||||
val := os.Getenv(envvarRPCURL)
|
|
||||||
if val == "" {
|
|
||||||
return nil, fmt.Errorf("envvar %q unset", envvarRPCURL)
|
|
||||||
}
|
|
||||||
connData, err := url.Parse(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"could not parse %q=%q: %v", envvarRPCURL, val, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
host := connData.Host
|
|
||||||
if connData.User == nil {
|
|
||||||
return nil, fmt.Errorf("no Userinfo in parsed url")
|
|
||||||
}
|
|
||||||
username := connData.User.Username()
|
|
||||||
password, ok := connData.User.Password()
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("no password given in RPC URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
connCfg := &rpcclient.ConnConfig{
|
|
||||||
Host: host,
|
|
||||||
User: username,
|
|
||||||
Pass: password,
|
|
||||||
HTTPPostMode: true,
|
|
||||||
DisableTLS: true,
|
|
||||||
}
|
|
||||||
return rpcclient.New(connCfg, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyHelloWorld(t *testing.T) {
|
|
||||||
if os.Getenv(envvarRPCURL) == "" {
|
|
||||||
t.Skipf("envvar %s unset, skipping", envvarRPCURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format RFC3339
|
|
||||||
expectedTime := "2015-05-28T15:41:18Z"
|
|
||||||
|
|
||||||
helloWorld, err := opentimestamps.NewDetachedTimestampFromPath(
|
|
||||||
"../examples/hello-world.txt.ots",
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
ts := helloWorld.Timestamp
|
|
||||||
|
|
||||||
btcConn, err := newTestBTCConn()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
verifier := BitcoinAttestationVerifier{btcConn}
|
|
||||||
|
|
||||||
// using BitcoinVerifications()
|
|
||||||
results := verifier.BitcoinVerifications(ts)
|
|
||||||
assert.Equal(t, 1, len(results))
|
|
||||||
result0 := results[0]
|
|
||||||
require.NoError(t, result0.Error)
|
|
||||||
assert.Equal(
|
|
||||||
t, expectedTime, result0.AttestationTime.Format(time.RFC3339),
|
|
||||||
)
|
|
||||||
|
|
||||||
// using Verify()
|
|
||||||
verifiedTime, err := verifier.Verify(ts)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, verifiedTime)
|
|
||||||
assert.Equal(t, expectedTime, verifiedTime.Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
26
commands.go
26
commands.go
@@ -1,26 +0,0 @@
|
|||||||
package opentimestamps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateDetachedTimestampForFile(
|
|
||||||
path string, cal *RemoteCalendar,
|
|
||||||
) (*DetachedTimestamp, error) {
|
|
||||||
f, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hasher := sha256.New()
|
|
||||||
if _, err := io.Copy(hasher, f); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
digest := hasher.Sum([]byte{})
|
|
||||||
ts, err := cal.Submit(digest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewDetachedTimestamp(*opSHA256, digest, ts)
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package opentimestamps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var fileHeaderMagic = []byte(
|
|
||||||
"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94",
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
minFileDigestLength = 20
|
|
||||||
maxFileDigestLength = 32
|
|
||||||
fileMajorVersion = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
type DetachedTimestamp struct {
|
|
||||||
HashOp cryptOp
|
|
||||||
FileHash []byte
|
|
||||||
Timestamp *Timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DetachedTimestamp) Dump() string {
|
|
||||||
w := &bytes.Buffer{}
|
|
||||||
fmt.Fprintf(
|
|
||||||
w, "File %s hash: %x\n", d.HashOp.name, d.Timestamp.Message,
|
|
||||||
)
|
|
||||||
fmt.Fprint(w, d.Timestamp.Dump())
|
|
||||||
return w.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DetachedTimestamp) encode(ctx *serializationContext) error {
|
|
||||||
if err := ctx.writeBytes(fileHeaderMagic); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ctx.writeVarUint(fileMajorVersion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := d.HashOp.encode(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := ctx.writeBytes(d.FileHash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.Timestamp.encode(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DetachedTimestamp) WriteToStream(w io.Writer) error {
|
|
||||||
return d.encode(&serializationContext{w})
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetachedTimestamp(
|
|
||||||
hashOp cryptOp, fileHash []byte, ts *Timestamp,
|
|
||||||
) (*DetachedTimestamp, error) {
|
|
||||||
if len(fileHash) != hashOp.digestLength {
|
|
||||||
return nil, fmt.Errorf(
|
|
||||||
"op %v expects %d byte digest, got %d",
|
|
||||||
hashOp, hashOp.digestLength, len(fileHash),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return &DetachedTimestamp{hashOp, fileHash, ts}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetachedTimestampFromReader(r io.Reader) (*DetachedTimestamp, error) {
|
|
||||||
ctx := newDeserializationContext(r)
|
|
||||||
if err := ctx.assertMagic([]byte(fileHeaderMagic)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
major, err := ctx.readVarUint()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if major != uint64(fileMajorVersion) {
|
|
||||||
return nil, fmt.Errorf("unexpected major version %d", major)
|
|
||||||
}
|
|
||||||
fileHashOp, err := parseCryptOp(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fileHash, err := ctx.readBytes(fileHashOp.digestLength)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ts, err := newTimestampFromContext(ctx, fileHash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &DetachedTimestamp{*fileHashOp, fileHash, ts}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDetachedTimestampFromPath(p string) (*DetachedTimestamp, error) {
|
|
||||||
f, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewDetachedTimestampFromReader(f)
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package opentimestamps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func examplePaths() []string {
|
|
||||||
matches, err := filepath.Glob("./examples/*ots")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return matches
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsUnknownAttestation(ts *Timestamp) (res bool) {
|
|
||||||
ts.Walk(func(subTs *Timestamp) {
|
|
||||||
for _, att := range subTs.Attestations {
|
|
||||||
if _, ok := att.(unknownAttestation); ok {
|
|
||||||
res = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeHelloWorld(t *testing.T) {
|
|
||||||
dts, err := NewDetachedTimestampFromPath(
|
|
||||||
"./examples/hello-world.txt.ots",
|
|
||||||
)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
attCount := 0
|
|
||||||
checkAttestation := func(ts *Timestamp, att Attestation) {
|
|
||||||
assert.Equal(t, 0, attCount)
|
|
||||||
|
|
||||||
expectedAtt := newBitcoinAttestation()
|
|
||||||
expectedAtt.Height = 358391
|
|
||||||
assert.Equal(t, expectedAtt, att)
|
|
||||||
|
|
||||||
// If ts.Message is correct, opcode parsing and execution should
|
|
||||||
// have succeeded.
|
|
||||||
assert.Equal(t,
|
|
||||||
"007ee445d23ad061af4a36b809501fab1ac4f2d7e7a739817dd0cbb7ec661b8a",
|
|
||||||
hex.EncodeToString(ts.Message),
|
|
||||||
)
|
|
||||||
|
|
||||||
attCount += 1
|
|
||||||
}
|
|
||||||
|
|
||||||
dts.Timestamp.Walk(func(ts *Timestamp) {
|
|
||||||
for _, att := range ts.Attestations {
|
|
||||||
// this should be called exactly once
|
|
||||||
checkAttestation(ts, att)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, 1, attCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeEncodeAll(t *testing.T) {
|
|
||||||
for _, path := range examplePaths() {
|
|
||||||
t.Log(path)
|
|
||||||
dts, err := NewDetachedTimestampFromPath(path)
|
|
||||||
assert.NoError(t, err, path)
|
|
||||||
|
|
||||||
if containsUnknownAttestation(dts.Timestamp) {
|
|
||||||
t.Logf("skipping encode cycle: unknownAttestation")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
|
||||||
err = dts.Timestamp.encode(&serializationContext{buf})
|
|
||||||
if !assert.NoError(t, err, path) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = bytes.NewBuffer(buf.Bytes())
|
|
||||||
ts1, err := NewTimestampFromReader(buf, dts.Timestamp.Message)
|
|
||||||
if !assert.NoError(t, err, path) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dts1, err := NewDetachedTimestamp(
|
|
||||||
dts.HashOp, dts.FileHash, ts1,
|
|
||||||
)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dts1Target := &bytes.Buffer{}
|
|
||||||
err = dts1.WriteToStream(dts1Target)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
orgBytes, err := ioutil.ReadFile(path)
|
|
||||||
if !assert.NoError(t, err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, orgBytes, dts1Target.Bytes())
|
|
||||||
t.Log("encode cycle success")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
esplora.go
Normal file
63
esplora.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package opentimestamps
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEsploraClient(url string) Bitcoin {
|
||||||
|
if strings.HasSuffix(url, "/") {
|
||||||
|
url = url[0 : len(url)-1]
|
||||||
|
}
|
||||||
|
return esplora{url}
|
||||||
|
}
|
||||||
|
|
||||||
|
type esplora struct{ baseurl string }
|
||||||
|
|
||||||
|
func (e esplora) GetBlockHash(height int64) (*chainhash.Hash, error) {
|
||||||
|
resp, err := http.Get(e.baseurl + "/block-height/" + strconv.FormatInt(height, 10))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
hexb, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := hex.Decode(hexb, hexb); err != nil || len(hexb) != chainhash.HashSize {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var chash chainhash.Hash
|
||||||
|
copy(chash[:], hexb)
|
||||||
|
return &chash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e esplora) GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error) {
|
||||||
|
resp, err := http.Get(fmt.Sprintf("%s/block/%x/header", e.baseurl, hash))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
hexb, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := hex.Decode(hexb, hexb); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header := &wire.BlockHeader{}
|
||||||
|
if err := header.BtcDecode(bytes.NewBuffer(hexb), 0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return header, nil
|
||||||
|
}
|
||||||
4
go.mod
4
go.mod
@@ -4,15 +4,14 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd v0.23.4
|
github.com/btcsuite/btcd v0.23.4
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
golang.org/x/crypto v0.13.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||||
github.com/btcsuite/btcd/btcutil v1.1.0 // indirect
|
github.com/btcsuite/btcd/btcutil v1.1.0 // indirect
|
||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
|
|
||||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||||
@@ -20,6 +19,7 @@ require (
|
|||||||
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
golang.org/x/crypto v0.13.0 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
golang.org/x/sys v0.12.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package opentimestamps
|
package opentimestamps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"golang.org/x/crypto/ripemd160"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxResultLength = 4096
|
const maxResultLength = 4096
|
||||||
@@ -49,25 +46,6 @@ func msgHexlify(msg []byte) ([]byte, error) {
|
|||||||
return []byte(hex.EncodeToString(msg)), nil
|
return []byte(hex.EncodeToString(msg)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func msgSHA1(msg []byte) ([]byte, error) {
|
|
||||||
res := sha1.Sum(msg)
|
|
||||||
return res[:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgRIPEMD160(msg []byte) ([]byte, error) {
|
|
||||||
h := ripemd160.New()
|
|
||||||
_, err := h.Write(msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return h.Sum([]byte{}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func msgSHA256(msg []byte) ([]byte, error) {
|
|
||||||
res := sha256.Sum256(msg)
|
|
||||||
return res[:], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type opCode interface {
|
type opCode interface {
|
||||||
match(byte) bool
|
match(byte) bool
|
||||||
decode(*deserializationContext) (opCode, error)
|
decode(*deserializationContext) (opCode, error)
|
||||||
@@ -110,30 +88,6 @@ func (u *unaryOp) apply(message []byte) ([]byte, error) {
|
|||||||
return u.msgOp(message)
|
return u.msgOp(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crypto operations
|
|
||||||
// These are hash ops that define a digest length
|
|
||||||
type cryptOp struct {
|
|
||||||
unaryOp
|
|
||||||
digestLength int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCryptOp(
|
|
||||||
tag byte, name string, msgOp unaryMsgOp, digestLength int,
|
|
||||||
) *cryptOp {
|
|
||||||
return &cryptOp{
|
|
||||||
unaryOp: *newUnaryOp(tag, name, msgOp),
|
|
||||||
digestLength: digestLength,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *cryptOp) decode(ctx *deserializationContext) (opCode, error) {
|
|
||||||
u, err := c.unaryOp.decode(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &cryptOp{*u.(*unaryOp), c.digestLength}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary operations
|
// Binary operations
|
||||||
// We decode an extra varbyte argument and use it in apply()
|
// We decode an extra varbyte argument and use it in apply()
|
||||||
|
|
||||||
@@ -179,20 +133,20 @@ func (b *binaryOp) String() string {
|
|||||||
return fmt.Sprintf("%s %x", b.name, b.argument)
|
return fmt.Sprintf("%s %x", b.name, b.argument)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func msgSHA256(msg []byte) ([]byte, error) {
|
||||||
|
res := sha256.Sum256(msg)
|
||||||
|
return res[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
opAppend = newBinaryOp(0xf0, "APPEND", msgAppend)
|
opAppend = newBinaryOp(0xf0, "APPEND", msgAppend)
|
||||||
opPrepend = newBinaryOp(0xf1, "PREPEND", msgPrepend)
|
opPrepend = newBinaryOp(0xf1, "PREPEND", msgPrepend)
|
||||||
opReverse = newUnaryOp(0xf2, "REVERSE", msgReverse)
|
opReverse = newUnaryOp(0xf2, "REVERSE", msgReverse)
|
||||||
opHexlify = newUnaryOp(0xf3, "HEXLIFY", msgHexlify)
|
opHexlify = newUnaryOp(0xf3, "HEXLIFY", msgHexlify)
|
||||||
opSHA1 = newCryptOp(0x02, "SHA1", msgSHA1, 20)
|
opSHA256 = newUnaryOp(0x08, "SHA256", msgSHA256)
|
||||||
opRIPEMD160 = newCryptOp(0x03, "RIPEMD160", msgRIPEMD160, 20)
|
|
||||||
opSHA256 = newCryptOp(0x08, "SHA256", msgSHA256, 32)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var opCodes []opCode = []opCode{
|
var opCodes []opCode = []opCode{opAppend, opPrepend, opReverse, opHexlify, opSHA256}
|
||||||
opAppend, opPrepend, opReverse, opHexlify, opSHA1, opRIPEMD160,
|
|
||||||
opSHA256,
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseOp(ctx *deserializationContext, tag byte) (opCode, error) {
|
func parseOp(ctx *deserializationContext, tag byte) (opCode, error) {
|
||||||
for _, op := range opCodes {
|
for _, op := range opCodes {
|
||||||
@@ -202,19 +156,3 @@ func parseOp(ctx *deserializationContext, tag byte) (opCode, error) {
|
|||||||
}
|
}
|
||||||
return nil, fmt.Errorf("could not decode tag %02x", tag)
|
return nil, fmt.Errorf("could not decode tag %02x", tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCryptOp(ctx *deserializationContext) (*cryptOp, error) {
|
|
||||||
tag, err := ctx.readByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
op, err := parseOp(ctx, tag)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if cryptOp, ok := op.(*cryptOp); ok {
|
|
||||||
return cryptOp, nil
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("expected cryptOp, got %#v", op)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package opentimestamps
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMsgAppend(t *testing.T) {
|
|
||||||
msg := []byte("123")
|
|
||||||
res, err := msgAppend(msg, []byte("456"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "123456", string(res))
|
|
||||||
// make sure changes to input msg don't affect output
|
|
||||||
msg[0] = byte('0')
|
|
||||||
assert.Equal(t, "123456", string(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgPrepend(t *testing.T) {
|
|
||||||
msg := []byte("123")
|
|
||||||
res, err := msgPrepend(msg, []byte("abc"))
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, "abc123", string(res))
|
|
||||||
// make sure changes to input msg don't affect output
|
|
||||||
msg[0] = byte('0')
|
|
||||||
assert.Equal(t, "abc123", string(res))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgReverse(t *testing.T) {
|
|
||||||
_, err := msgReverse([]byte{})
|
|
||||||
assert.Error(t, err)
|
|
||||||
res, err := msgReverse([]byte{1, 2, 3})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, []byte{3, 2, 1}, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgHexlify(t *testing.T) {
|
|
||||||
_, err := msgHexlify([]byte{})
|
|
||||||
assert.Error(t, err)
|
|
||||||
res, err := msgHexlify([]byte{1, 2, 3, 0xff})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, []byte("010203ff"), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgSHA1(t *testing.T) {
|
|
||||||
out, err := msgSHA1([]byte{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t,
|
|
||||||
"da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
|
||||||
hex.EncodeToString(out),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMsgSHA256(t *testing.T) {
|
|
||||||
out, err := msgSHA256([]byte{})
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t,
|
|
||||||
"e3b0c44298fc1c149afbf4c8996fb924"+
|
|
||||||
"27ae41e4649b934ca495991b7852b855",
|
|
||||||
hex.EncodeToString(out),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRIPEMD160(t *testing.T) {
|
|
||||||
out, err := msgRIPEMD160([]byte{})
|
|
||||||
assert.Equal(t,
|
|
||||||
"9c1185a5c5e9fc54612808977ee8f548b2258d31",
|
|
||||||
hex.EncodeToString(out),
|
|
||||||
)
|
|
||||||
|
|
||||||
out, err = msgRIPEMD160(out)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t,
|
|
||||||
"38bbc57e4cbe8b6a1d2c999ef62503e0a6e58109",
|
|
||||||
hex.EncodeToString(out),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -49,7 +49,7 @@ func checkStatusOK(resp *http.Response) error {
|
|||||||
return fmt.Errorf("%s (body=nil)", errMsg)
|
return fmt.Errorf("%s (body=nil)", errMsg)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%s (bodyErr=%v)", errMsg, err)
|
return fmt.Errorf("%s (bodyErr=%v)", errMsg, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -80,8 +80,8 @@ func (c *RemoteCalendar) url(path string) string {
|
|||||||
return c.baseURL + path
|
return c.baseURL + path
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteCalendar) Submit(digest []byte) (*Timestamp, error) {
|
func (c *RemoteCalendar) Submit(digest [32]byte) (*Timestamp, error) {
|
||||||
body := bytes.NewBuffer(digest)
|
body := bytes.NewBuffer(digest[:])
|
||||||
req, err := http.NewRequest("POST", c.url("digest"), body)
|
req, err := http.NewRequest("POST", c.url("digest"), body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -96,7 +96,7 @@ func (c *RemoteCalendar) Submit(digest []byte) (*Timestamp, error) {
|
|||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("expected 200, got %v", resp.Status)
|
return nil, fmt.Errorf("expected 200, got %v", resp.Status)
|
||||||
}
|
}
|
||||||
return NewTimestampFromReader(resp.Body, digest)
|
return NewTimestampFromReader(resp.Body, digest[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RemoteCalendar) GetTimestamp(commitment []byte) (*Timestamp, error) {
|
func (c *RemoteCalendar) GetTimestamp(commitment []byte) (*Timestamp, error) {
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ func newTestCalendar(url string) *RemoteCalendar {
|
|||||||
return cal
|
return cal
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestDigest(in string) []byte {
|
func newTestDigest(in string) [32]byte {
|
||||||
hash := sha256.Sum256([]byte(in))
|
return sha256.Sum256([]byte(in))
|
||||||
return hash[:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteCalendarExample(t *testing.T) {
|
func TestRemoteCalendarExample(t *testing.T) {
|
||||||
|
|||||||
16
timestamp.go
16
timestamp.go
@@ -114,13 +114,7 @@ func (t *Timestamp) Dump() string {
|
|||||||
return t.DumpWithConfig(defaultDumpConfig)
|
return t.DumpWithConfig(defaultDumpConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTagOrAttestation(
|
func parseTagOrAttestation(ts *Timestamp, ctx *deserializationContext, tag byte, message []byte, limit int) error {
|
||||||
ts *Timestamp,
|
|
||||||
ctx *deserializationContext,
|
|
||||||
tag byte,
|
|
||||||
message []byte,
|
|
||||||
limit int,
|
|
||||||
) error {
|
|
||||||
if tag == 0x00 {
|
if tag == 0x00 {
|
||||||
a, err := ParseAttestation(ctx)
|
a, err := ParseAttestation(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,9 +141,7 @@ func parseTagOrAttestation(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(
|
func parse(ts *Timestamp, ctx *deserializationContext, message []byte, limit int) error {
|
||||||
ts *Timestamp, ctx *deserializationContext, message []byte, limit int,
|
|
||||||
) error {
|
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
return fmt.Errorf("recursion limit")
|
return fmt.Errorf("recursion limit")
|
||||||
}
|
}
|
||||||
@@ -176,9 +168,7 @@ func parse(
|
|||||||
return parseTagOrAttestation(ts, ctx, tag, message, limit)
|
return parseTagOrAttestation(ts, ctx, tag, message, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTimestampFromContext(
|
func newTimestampFromContext(ctx *deserializationContext, message []byte) (*Timestamp, error) {
|
||||||
ctx *deserializationContext, message []byte,
|
|
||||||
) (*Timestamp, error) {
|
|
||||||
recursionLimit := 1000
|
recursionLimit := 1000
|
||||||
ts := &Timestamp{Message: message}
|
ts := &Timestamp{Message: message}
|
||||||
err := parse(ts, ctx, message, recursionLimit)
|
err := parse(ts, ctx, message, recursionLimit)
|
||||||
|
|||||||
@@ -1,40 +1,31 @@
|
|||||||
package client
|
package opentimestamps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fiatjaf/opentimestamps"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/rpcclient"
|
"github.com/btcsuite/btcd/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A BitcoinAttestationVerifier uses a bitcoin RPC connection to verify bitcoin
|
type Bitcoin interface {
|
||||||
// headers.
|
GetBlockHash(height int64) (*chainhash.Hash, error)
|
||||||
type BitcoinAttestationVerifier struct {
|
GetBlockHeader(hash *chainhash.Hash) (*wire.BlockHeader, error)
|
||||||
btcrpcClient *rpcclient.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBitcoinAttestationVerifier(
|
|
||||||
c *rpcclient.Client,
|
|
||||||
) *BitcoinAttestationVerifier {
|
|
||||||
return &BitcoinAttestationVerifier{c}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyAttestation checks a BitcoinAttestation using a given hash digest. It
|
// VerifyAttestation checks a BitcoinAttestation using a given hash digest. It
|
||||||
// returns the time of the block if the verification succeeds, an error
|
// returns the time of the block if the verification succeeds, an error
|
||||||
// otherwise.
|
// otherwise.
|
||||||
func (v *BitcoinAttestationVerifier) VerifyAttestation(
|
func VerifyAttestation(bitcoinInterface Bitcoin, digest []byte, a *BitcoinAttestation) (*time.Time, error) {
|
||||||
digest []byte, a *opentimestamps.BitcoinAttestation,
|
|
||||||
) (*time.Time, error) {
|
|
||||||
if a.Height > math.MaxInt64 {
|
if a.Height > math.MaxInt64 {
|
||||||
return nil, fmt.Errorf("illegal block height")
|
return nil, fmt.Errorf("illegal block height")
|
||||||
}
|
}
|
||||||
blockHash, err := v.btcrpcClient.GetBlockHash(int64(a.Height))
|
blockHash, err := bitcoinInterface.GetBlockHash(int64(a.Height))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
h, err := v.btcrpcClient.GetBlockHeader(blockHash)
|
h, err := bitcoinInterface.GetBlockHeader(blockHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -51,24 +42,22 @@ func (v *BitcoinAttestationVerifier) VerifyAttestation(
|
|||||||
|
|
||||||
// A BitcoinVerification is the result of verifying a BitcoinAttestation
|
// A BitcoinVerification is the result of verifying a BitcoinAttestation
|
||||||
type BitcoinVerification struct {
|
type BitcoinVerification struct {
|
||||||
Timestamp *opentimestamps.Timestamp
|
Timestamp *Timestamp
|
||||||
Attestation *opentimestamps.BitcoinAttestation
|
Attestation *BitcoinAttestation
|
||||||
AttestationTime *time.Time
|
AttestationTime *time.Time
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
// BitcoinVerifications returns the all bitcoin attestation results for the
|
// BitcoinVerifications returns the all bitcoin attestation results for the
|
||||||
// timestamp.
|
// timestamp.
|
||||||
func (v *BitcoinAttestationVerifier) BitcoinVerifications(
|
func BitcoinVerifications(bitcoinInterface Bitcoin, t *Timestamp) (res []BitcoinVerification) {
|
||||||
t *opentimestamps.Timestamp,
|
t.Walk(func(ts *Timestamp) {
|
||||||
) (res []BitcoinVerification) {
|
|
||||||
t.Walk(func(ts *opentimestamps.Timestamp) {
|
|
||||||
for _, att := range ts.Attestations {
|
for _, att := range ts.Attestations {
|
||||||
btcAtt, ok := att.(*opentimestamps.BitcoinAttestation)
|
btcAtt, ok := att.(*BitcoinAttestation)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
attTime, err := v.VerifyAttestation(ts.Message, btcAtt)
|
attTime, err := VerifyAttestation(bitcoinInterface, ts.Message, btcAtt)
|
||||||
res = append(res, BitcoinVerification{
|
res = append(res, BitcoinVerification{
|
||||||
Timestamp: ts,
|
Timestamp: ts,
|
||||||
Attestation: btcAtt,
|
Attestation: btcAtt,
|
||||||
@@ -82,10 +71,8 @@ func (v *BitcoinAttestationVerifier) BitcoinVerifications(
|
|||||||
|
|
||||||
// Verify returns the earliest bitcoin-attested time, or nil if none can be
|
// Verify returns the earliest bitcoin-attested time, or nil if none can be
|
||||||
// found or verified successfully.
|
// found or verified successfully.
|
||||||
func (v *BitcoinAttestationVerifier) Verify(
|
func Verify(bitcoinInterface Bitcoin, t *Timestamp) (ret *time.Time, err error) {
|
||||||
t *opentimestamps.Timestamp,
|
res := BitcoinVerifications(bitcoinInterface, t)
|
||||||
) (ret *time.Time, err error) {
|
|
||||||
res := v.BitcoinVerifications(t)
|
|
||||||
for _, r := range res {
|
for _, r := range res {
|
||||||
if r.Error != nil {
|
if r.Error != nil {
|
||||||
err = r.Error
|
err = r.Error
|
||||||
Reference in New Issue
Block a user