go_study/fabric-main/cmd/osnadmin/main_test.go

838 lines
24 KiB
Go

/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"github.com/golang/protobuf/proto"
cb "github.com/hyperledger/fabric-protos-go/common"
"github.com/hyperledger/fabric/bccsp"
"github.com/hyperledger/fabric/cmd/osnadmin/mocks"
"github.com/hyperledger/fabric/common/crypto/tlsgen"
"github.com/hyperledger/fabric/orderer/common/channelparticipation"
"github.com/hyperledger/fabric/orderer/common/localconfig"
"github.com/hyperledger/fabric/orderer/common/types"
"github.com/hyperledger/fabric/protoutil"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("osnadmin", func() {
var (
tempDir string
ordererCACert string
clientCert string
clientKey string
mockChannelManagement *mocks.ChannelManagement
testServer *httptest.Server
tlsConfig *tls.Config
ordererURL string
channelID string
)
BeforeEach(func() {
var err error
tempDir, err = os.MkdirTemp("", "osnadmin")
Expect(err).NotTo(HaveOccurred())
generateCertificates(tempDir)
ordererCACert = filepath.Join(tempDir, "server-ca.pem")
clientCert = filepath.Join(tempDir, "client-cert.pem")
clientKey = filepath.Join(tempDir, "client-key.pem")
channelID = "testing123"
config := localconfig.ChannelParticipation{
Enabled: true,
MaxRequestBodySize: 1024 * 1024,
}
mockChannelManagement = &mocks.ChannelManagement{}
h := channelparticipation.NewHTTPHandler(config, mockChannelManagement)
Expect(h).NotTo(BeNil())
testServer = httptest.NewUnstartedServer(h)
cert, err := tls.LoadX509KeyPair(
filepath.Join(tempDir, "server-cert.pem"),
filepath.Join(tempDir, "server-key.pem"),
)
Expect(err).NotTo(HaveOccurred())
caCertPool := x509.NewCertPool()
clientCAPem, err := ioutil.ReadFile(filepath.Join(tempDir, "client-ca.pem"))
Expect(err).NotTo(HaveOccurred())
caCertPool.AppendCertsFromPEM(clientCAPem)
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
})
JustBeforeEach(func() {
if tlsConfig != nil {
testServer.TLS = tlsConfig
testServer.StartTLS()
} else {
testServer.Start()
}
u, err := url.Parse(testServer.URL)
Expect(err).NotTo(HaveOccurred())
ordererURL = u.Host
})
AfterEach(func() {
os.RemoveAll(tempDir)
testServer.Close()
})
Describe("List", func() {
BeforeEach(func() {
mockChannelManagement.ChannelListReturns(types.ChannelList{
Channels: []types.ChannelInfoShort{
{
Name: "participation-trophy",
},
{
Name: "another-participation-trophy",
},
},
SystemChannel: &types.ChannelInfoShort{
Name: "fight-the-system",
},
})
mockChannelManagement.ChannelInfoReturns(types.ChannelInfo{
Name: "asparagus",
ConsensusRelation: "broccoli",
Status: "carrot",
Height: 987,
}, nil)
})
It("uses the channel participation API to list all application channels and the system channel (when it exists)", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelList{
Channels: []types.ChannelInfoShort{
{
Name: "participation-trophy",
URL: "/participation/v1/channels/participation-trophy",
},
{
Name: "another-participation-trophy",
URL: "/participation/v1/channels/another-participation-trophy",
},
},
SystemChannel: &types.ChannelInfoShort{
Name: "fight-the-system",
URL: "/participation/v1/channels/fight-the-system",
},
}
checkStatusOutput(output, exit, err, 200, expectedOutput)
})
It("uses the channel participation API to list the details of a single channel", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--channelID", "tell-me-your-secrets",
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelInfo{
Name: "asparagus",
URL: "/participation/v1/channels/asparagus",
ConsensusRelation: "broccoli",
Status: "carrot",
Height: 987,
}
checkStatusOutput(output, exit, err, 200, expectedOutput)
})
Context("when the channel does not exist", func() {
BeforeEach(func() {
mockChannelManagement.ChannelInfoReturns(types.ChannelInfo{}, errors.New("eat-your-peas"))
})
It("returns 404 not found", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--channelID", "tell-me-your-secrets",
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ErrorResponse{
Error: "eat-your-peas",
}
checkStatusOutput(output, exit, err, 404, expectedOutput)
})
})
Context("when TLS is disabled", func() {
BeforeEach(func() {
tlsConfig = nil
})
It("uses the channel participation API to list all channels", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
expectedOutput := types.ChannelList{
Channels: []types.ChannelInfoShort{
{
Name: "participation-trophy",
URL: "/participation/v1/channels/participation-trophy",
},
{
Name: "another-participation-trophy",
URL: "/participation/v1/channels/another-participation-trophy",
},
},
SystemChannel: &types.ChannelInfoShort{
Name: "fight-the-system",
URL: "/participation/v1/channels/fight-the-system",
},
}
checkStatusOutput(output, exit, err, 200, expectedOutput)
})
It("uses the channel participation API to list the details of a single channel", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--channelID", "tell-me-your-secrets",
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
expectedOutput := types.ChannelInfo{
Name: "asparagus",
URL: "/participation/v1/channels/asparagus",
ConsensusRelation: "broccoli",
Status: "carrot",
Height: 987,
}
checkStatusOutput(output, exit, err, 200, expectedOutput)
})
})
})
Describe("Remove", func() {
It("uses the channel participation API to remove a channel", func() {
args := []string{
"channel",
"remove",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
Expect(output).To(Equal("Status: 204\n"))
})
It("uses the channel participation API to remove a channel (without status)", func() {
args := []string{
"channel",
"remove",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
"--no-status",
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
Expect(output).To(BeEmpty())
})
Context("when the channel does not exist", func() {
BeforeEach(func() {
mockChannelManagement.RemoveChannelReturns(types.ErrChannelNotExist)
})
It("returns 404 not found", func() {
args := []string{
"channel",
"remove",
"--ca-file", ordererCACert,
"--orderer-address", ordererURL,
"--channelID", channelID,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ErrorResponse{
Error: "cannot remove: channel does not exist",
}
checkStatusOutput(output, exit, err, 404, expectedOutput)
})
})
Context("when TLS is disabled", func() {
BeforeEach(func() {
tlsConfig = nil
})
It("uses the channel participation API to remove a channel", func() {
args := []string{
"channel",
"remove",
"--orderer-address", ordererURL,
"--channelID", channelID,
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
Expect(output).To(Equal("Status: 204\n"))
})
})
})
Describe("Join", func() {
var blockPath string
BeforeEach(func() {
configBlock := blockWithGroups(
map[string]*cb.ConfigGroup{
"Application": {},
},
"testing123",
)
blockPath = createBlockFile(tempDir, configBlock)
mockChannelManagement.JoinChannelReturns(types.ChannelInfo{
Name: "apple",
ConsensusRelation: "banana",
Status: "orange",
Height: 123,
}, nil)
})
It("uses the channel participation API to join a channel", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelInfo{
Name: "apple",
URL: "/participation/v1/channels/apple",
ConsensusRelation: "banana",
Status: "orange",
Height: 123,
}
checkStatusOutput(output, exit, err, 201, expectedOutput)
})
Context("when the block is empty", func() {
BeforeEach(func() {
blockPath = createBlockFile(tempDir, &cb.Block{})
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "failed to retrieve channel id - block is empty")
})
})
Context("when the --channelID does not match the channel ID in the block", func() {
BeforeEach(func() {
channelID = "not-the-channel-youre-looking-for"
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "specified --channelID not-the-channel-youre-looking-for does not match channel ID testing123 in config block")
})
})
Context("when the block isn't a valid config block", func() {
BeforeEach(func() {
block := &cb.Block{
Data: &cb.BlockData{
Data: [][]byte{
protoutil.MarshalOrPanic(&cb.Envelope{
Payload: protoutil.MarshalOrPanic(&cb.Payload{
Header: &cb.Header{
ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
Type: int32(cb.HeaderType_ENDORSER_TRANSACTION),
ChannelId: channelID,
}),
},
}),
}),
},
},
}
blockPath = createBlockFile(tempDir, block)
})
It("returns 405 bad request", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
expectedOutput := types.ErrorResponse{
Error: "invalid join block: block is not a config block",
}
checkStatusOutput(output, exit, err, 400, expectedOutput)
})
})
Context("when joining the channel fails", func() {
BeforeEach(func() {
mockChannelManagement.JoinChannelReturns(types.ChannelInfo{}, types.ErrChannelAlreadyExists)
})
It("returns 405 not allowed", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ErrorResponse{
Error: "cannot join: channel already exists",
}
checkStatusOutput(output, exit, err, 405, expectedOutput)
})
It("returns 405 not allowed (without status)", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
"--no-status",
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ErrorResponse{
Error: "cannot join: channel already exists",
}
checkOutput(output, exit, err, expectedOutput)
})
})
Context("when TLS is disabled", func() {
BeforeEach(func() {
tlsConfig = nil
})
It("uses the channel participation API to join a channel", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--config-block", blockPath,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelInfo{
Name: "apple",
URL: "/participation/v1/channels/apple",
ConsensusRelation: "banana",
Status: "orange",
Height: 123,
}
checkStatusOutput(output, exit, err, 201, expectedOutput)
})
})
})
Describe("Flags", func() {
It("accepts short versions of the --orderer-address, --channelID, and --config-block flags", func() {
configBlock := blockWithGroups(
map[string]*cb.ConfigGroup{
"Application": {},
},
"testing123",
)
blockPath := createBlockFile(tempDir, configBlock)
mockChannelManagement.JoinChannelReturns(types.ChannelInfo{
Name: "apple",
ConsensusRelation: "banana",
Status: "orange",
Height: 123,
}, nil)
args := []string{
"channel",
"join",
"-o", ordererURL,
"-c", channelID,
"-b", blockPath,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelInfo{
Name: "apple",
URL: "/participation/v1/channels/apple",
ConsensusRelation: "banana",
Status: "orange",
Height: 123,
}
checkStatusOutput(output, exit, err, 201, expectedOutput)
})
Context("when an unknown flag is used", func() {
It("returns an error for long flags", func() {
_, _, err := executeForArgs([]string{"channel", "list", "--bad-flag"})
Expect(err).To(MatchError("unknown long flag '--bad-flag'"))
})
It("returns an error for short flags", func() {
_, _, err := executeForArgs([]string{"channel", "list", "-z"})
Expect(err).To(MatchError("unknown short flag '-z'"))
})
})
Context("when the ca cert cannot be read", func() {
BeforeEach(func() {
ordererCACert = "not-the-ca-cert-youre-looking-for"
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "reading orderer CA certificate: open not-the-ca-cert-youre-looking-for: no such file or directory")
})
})
Context("when the ca-file contains a private key instead of certificate(s)", func() {
BeforeEach(func() {
ordererCACert = clientKey
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"remove",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "failed to add ca-file PEM to cert pool")
})
})
Context("when the client cert/key pair fail to load", func() {
BeforeEach(func() {
clientKey = "brussel-sprouts"
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "loading client cert/key pair: open brussel-sprouts: no such file or directory")
})
})
Context("when the config block cannot be read", func() {
var configBlockPath string
BeforeEach(func() {
configBlockPath = "not-the-config-block-youre-looking-for"
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"join",
"--orderer-address", ordererURL,
"--channelID", channelID,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
"--config-block", configBlockPath,
}
output, exit, err := executeForArgs(args)
checkFlagError(output, exit, err, "reading config block: open not-the-config-block-youre-looking-for: no such file or directory")
})
})
})
Describe("Server using intermediate CA", func() {
BeforeEach(func() {
cert, err := tls.LoadX509KeyPair(
filepath.Join(tempDir, "server-intermediate-cert.pem"),
filepath.Join(tempDir, "server-intermediate-key.pem"),
)
Expect(err).NotTo(HaveOccurred())
tlsConfig.Certificates = []tls.Certificate{cert}
ordererCACert = filepath.Join(tempDir, "server-ca+intermediate-ca.pem")
})
It("uses the channel participation API to list all application and the system channel (when it exists)", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
expectedOutput := types.ChannelList{
Channels: nil,
SystemChannel: nil,
}
checkStatusOutput(output, exit, err, 200, expectedOutput)
})
Context("when the ca-file does not include the intermediate CA", func() {
BeforeEach(func() {
ordererCACert = filepath.Join(tempDir, "server-ca.pem")
})
It("returns with exit code 1 and prints the error", func() {
args := []string{
"channel",
"list",
"--orderer-address", ordererURL,
"--ca-file", ordererCACert,
"--client-cert", clientCert,
"--client-key", clientKey,
}
output, exit, err := executeForArgs(args)
checkCLIError(output, exit, err, fmt.Sprintf("Get \"%s/participation/v1/channels\": tls: failed to verify certificate: x509: certificate signed by unknown authority", testServer.URL))
})
})
})
})
func checkStatusOutput(output string, exit int, err error, expectedStatus int, expectedOutput interface{}) {
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
json, err := json.MarshalIndent(expectedOutput, "", "\t")
Expect(err).NotTo(HaveOccurred())
Expect(output).To(Equal(fmt.Sprintf("Status: %d\n%s\n", expectedStatus, string(json))))
}
func checkOutput(output string, exit int, err error, expectedOutput interface{}) {
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(0))
json, err := json.MarshalIndent(expectedOutput, "", "\t")
Expect(err).NotTo(HaveOccurred())
Expect(output).To(Equal(string(json) + "\n"))
}
func checkFlagError(output string, exit int, err error, expectedError string) {
Expect(err).To(MatchError(ContainSubstring(expectedError)))
Expect(exit).To(Equal(1))
Expect(output).To(BeEmpty())
}
func checkCLIError(output string, exit int, err error, expectedError string) {
Expect(err).NotTo(HaveOccurred())
Expect(exit).To(Equal(1))
Expect(output).To(Equal(fmt.Sprintf("Error: %s\n", expectedError)))
}
func generateCertificates(tempDir string) {
serverCA, err := tlsgen.NewCA()
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-ca.pem"), serverCA.CertBytes(), 0o640)
Expect(err).NotTo(HaveOccurred())
serverKeyPair, err := serverCA.NewServerCertKeyPair("127.0.0.1")
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-cert.pem"), serverKeyPair.Cert, 0o640)
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-key.pem"), serverKeyPair.Key, 0o640)
Expect(err).NotTo(HaveOccurred())
serverIntermediateCA, err := serverCA.NewIntermediateCA()
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-intermediate-ca.pem"), serverIntermediateCA.CertBytes(), 0o640)
Expect(err).NotTo(HaveOccurred())
serverIntermediateKeyPair, err := serverIntermediateCA.NewServerCertKeyPair("127.0.0.1")
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-intermediate-cert.pem"), serverIntermediateKeyPair.Cert, 0o640)
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "server-intermediate-key.pem"), serverIntermediateKeyPair.Key, 0o640)
Expect(err).NotTo(HaveOccurred())
serverAndIntermediateCABytes := append(serverCA.CertBytes(), serverIntermediateCA.CertBytes()...)
err = ioutil.WriteFile(filepath.Join(tempDir, "server-ca+intermediate-ca.pem"), serverAndIntermediateCABytes, 0o640)
Expect(err).NotTo(HaveOccurred())
clientCA, err := tlsgen.NewCA()
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "client-ca.pem"), clientCA.CertBytes(), 0o640)
Expect(err).NotTo(HaveOccurred())
clientKeyPair, err := clientCA.NewClientCertKeyPair()
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "client-cert.pem"), clientKeyPair.Cert, 0o640)
Expect(err).NotTo(HaveOccurred())
err = ioutil.WriteFile(filepath.Join(tempDir, "client-key.pem"), clientKeyPair.Key, 0o640)
Expect(err).NotTo(HaveOccurred())
}
func blockWithGroups(groups map[string]*cb.ConfigGroup, channelID string) *cb.Block {
block := protoutil.NewBlock(0, []byte{})
block.Data = &cb.BlockData{
Data: [][]byte{
protoutil.MarshalOrPanic(&cb.Envelope{
Payload: protoutil.MarshalOrPanic(&cb.Payload{
Data: protoutil.MarshalOrPanic(&cb.ConfigEnvelope{
Config: &cb.Config{
ChannelGroup: &cb.ConfigGroup{
Groups: groups,
Values: map[string]*cb.ConfigValue{
"HashingAlgorithm": {
Value: protoutil.MarshalOrPanic(&cb.HashingAlgorithm{
Name: bccsp.SHA256,
}),
},
"BlockDataHashingStructure": {
Value: protoutil.MarshalOrPanic(&cb.BlockDataHashingStructure{
Width: math.MaxUint32,
}),
},
"OrdererAddresses": {
Value: protoutil.MarshalOrPanic(&cb.OrdererAddresses{
Addresses: []string{"localhost"},
}),
},
},
},
},
}),
Header: &cb.Header{
ChannelHeader: protoutil.MarshalOrPanic(&cb.ChannelHeader{
Type: int32(cb.HeaderType_CONFIG),
ChannelId: channelID,
}),
},
}),
}),
},
}
block.Header.DataHash = protoutil.BlockDataHash(block.Data)
protoutil.InitBlockMetadata(block)
return block
}
func createBlockFile(tempDir string, configBlock *cb.Block) string {
blockBytes, err := proto.Marshal(configBlock)
Expect(err).NotTo(HaveOccurred())
blockPath := filepath.Join(tempDir, "block.pb")
err = ioutil.WriteFile(blockPath, blockBytes, 0o644)
Expect(err).NotTo(HaveOccurred())
return blockPath
}