From 11d91a4e58291d970e8384adc57b5f859a137e09 Mon Sep 17 00:00:00 2001 From: Eugene Zagidullin Date: Thu, 6 Jul 2023 19:44:46 +0300 Subject: [PATCH] Watermark order issue (#385) * watermark order is gone, watermark v2 is introduced * Edo block blobs removed from unit tests * fix watermark integration tests in resolving order issue --------- Co-authored-by: stephengaudet --- .gitignore | 5 +- cmd/approve-list-svc/server/server_test.go | 2 +- cmd/commands/root.go | 7 +- integration_test/.watermarks/.gitignore | 4 + integration_test/docker-compose.yml | 2 + integration_test/watermark_test.go | 139 ++++------ pkg/signatory/policy_hook_test.go | 4 +- pkg/signatory/request/request.go | 165 +++--------- pkg/signatory/request/request_test.go | 2 +- pkg/signatory/request/watermark_test.go | 288 +++++---------------- pkg/signatory/signatory.go | 3 +- pkg/signatory/signatory_test.go | 8 +- pkg/signatory/watermark_file.go | 202 ++++++++------- pkg/signatory/watermark_mem.go | 41 +-- pkg/signatory/watermark_migration.go | 170 ++++++++++++ pkg/signatory/watermark_test.go | 116 +++++---- 16 files changed, 549 insertions(+), 609 deletions(-) create mode 100644 integration_test/.watermarks/.gitignore create mode 100644 pkg/signatory/watermark_migration.go diff --git a/.gitignore b/.gitignore index fcb4dbe7..4c185b5e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,9 @@ dist .release-env .env .docker-creds -signatory -signatory-cli +./signatory +./signatory-cli +.DS_Store # some integration_tests write secret env var to files integration_test/gcp-token.json diff --git a/cmd/approve-list-svc/server/server_test.go b/cmd/approve-list-svc/server/server_test.go index 86d0c8a9..fbceda3c 100644 --- a/cmd/approve-list-svc/server/server_test.go +++ b/cmd/approve-list-svc/server/server_test.go @@ -80,7 +80,7 @@ func testServer(t *testing.T, addr []net.IP) error { s, err := signatory.New(context.Background(), &conf) require.NoError(t, err) - msg, _ := hex.DecodeString("019caecab9000753d3029bc7d9a36b60cce68ade985a0a16929587166e0d3de61efff2fa31b7116bf670000000005ee3c23b04519d71c4e54089c56773c44979b3ba3d61078ade40332ad81577ae074f653e0e0000001100000001010000000800000000000753d2da051ba81185783e4cbc633cf2ba809139ef07c3e5f6c5867f930e7667b224430000cde7fbbb948e030000") + msg, _ := hex.DecodeString("11ed9d217c0000518e0118425847ac255b6d7c30ce8fec23b8eaf13b741de7d18509ac2ef83c741209630000000061947af504805682ea5d089837764b3efcc90b91db24294ff9ddb66019f332ccba17cc4741000000210000000102000000040000518e0000000000000004ffffffff0000000400000000eb1320a71e8bf8b0162a3ec315461e9153a38b70d00d5dde2df85eb92748f8d068d776e356683a9e23c186ccfb72ddc6c9857bb1704487972922e7c89a7121f800000000a8e1dd3c000000000000") _, err = s.Sign(context.Background(), &signatory.SignRequest{PublicKeyHash: signKeyHash, Message: msg, Source: net.IPv6loopback}) return err } diff --git a/cmd/commands/root.go b/cmd/commands/root.go index 87a6ea71..fca72b00 100644 --- a/cmd/commands/root.go +++ b/cmd/commands/root.go @@ -78,11 +78,16 @@ func NewRootCommand(c *Context, name string) *cobra.Command { return err } + watermark, err := signatory.NewFileWatermark(baseDir) + if err != nil { + return err + } + sigConf := signatory.Config{ Policy: pol, Vaults: conf.Vaults, Interceptor: metrics.Interceptor, - Watermark: &signatory.FileWatermark{BaseDir: baseDir}, + Watermark: watermark, } if conf.PolicyHook != nil && conf.PolicyHook.Address != "" { diff --git a/integration_test/.watermarks/.gitignore b/integration_test/.watermarks/.gitignore new file mode 100644 index 00000000..5e7d2734 --- /dev/null +++ b/integration_test/.watermarks/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/integration_test/docker-compose.yml b/integration_test/docker-compose.yml index b176ed13..b25a49e1 100644 --- a/integration_test/docker-compose.yml +++ b/integration_test/docker-compose.yml @@ -72,6 +72,8 @@ services: - "9583:9583" networks: - ecadnet + volumes: + - ./.watermarks:/var/lib/signatory configs: - source: sigy-config target: /etc/signatory.yaml diff --git a/integration_test/watermark_test.go b/integration_test/watermark_test.go index 01cbb1fb..31e36ece 100644 --- a/integration_test/watermark_test.go +++ b/integration_test/watermark_test.go @@ -14,12 +14,10 @@ import ( "github.com/stretchr/testify/require" ) -// type keyToWatermark map[string]keyToWatermark -type keyToWatermark map[string]*watermarkData +type watermarkFile map[string]map[string]*watermarkData type watermarkData struct { Round int32 `json:"round,omitempty"` Level int32 `json:"level"` - Order int32 `json:"order,omitempty"` Hash string `json:"hash"` } @@ -29,7 +27,7 @@ const ( port = "6732" pkh = "tz1WGcYos3hL7GXYXjKrMnSFdkT7FyXnFBvf" url = protocol + host + ":" + port + "/keys/" + pkh - dir = "/var/lib/signatory/watermark_v1/" + dir = "/var/lib/signatory/watermark_v2/" container = "signatory" ) @@ -38,13 +36,12 @@ type functionalTestCase struct { signRequestBodies []string expectedStatusCodes []int expectedResponses []any - watermarkBefore keyToWatermark - watermarkAfter keyToWatermark + watermarkBefore watermarkFile + watermarkAfter watermarkFile chainID string } var functionalTestCases = []functionalTestCase{ - {title: "watermark file is created if it does not exist", signRequestBodies: []string{ "\"11b3d79f99000000020130c1cb51f36daebee85fe99c04800a38e8133ffd2fa329cd4db35f32fe5bf5e30000000064277aa504683625c2445a4e9564bf710c5528fd99a7d150d2a2a323bc22ff9e2710da4f6d00000021000000010200000004000000020000000000000004ffffffff0000000400000000080966c1f5a955161345bc7d81ac205ebafc89f5977a5bc88e47ab1b6f8d791e5ae8b92d9bc0523b3e07848458e66dc4265e29f3c5d8007447862e2483fdad1200000000a40d1a28000000000002\"", @@ -54,7 +51,7 @@ var functionalTestCases = []functionalTestCase{ expectedResponses: []any{SuccessResponse{Signature: "edsigu6FPqZdPhLNo5AGCkdscaiMdZXsiV5djxy1v2r89J1ZWfVfZ2UXJEcURSsx38JSHXtccr9o5yzJ46NL6mGEZbZ86fiJjuv"}, []FailureResponse{{Id: "failure", Kind: "temporary", Msg: "watermark validation failed"}}}, watermarkBefore: nil, - watermarkAfter: keyToWatermark{pkh: {Level: 2, Round: 0, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 2, Round: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "existing watermark file is honoured and updated", @@ -66,8 +63,8 @@ var functionalTestCases = []functionalTestCase{ expectedStatusCodes: []int{200, 409}, expectedResponses: []any{SuccessResponse{Signature: "edsigtbYeDN9n2VjpoCmPVt5tQBe6Y95wTPPpgtVTx3g2S4hQSwypcdBWm5s6gfTi567MFFpDqXPRmdUBo4VqseLZdZc8LWnvK9"}, []FailureResponse{{Id: "failure", Kind: "temporary", Msg: "watermark validation failed"}}}, - watermarkBefore: keyToWatermark{pkh: {Level: 6, Round: 0, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 7, Round: 2, Order: 0, Hash: "vh2x7GpWVr8wSUE7HuAr5antYqgqzXFTDA4Wy6UUAMTzvNU1cnd4"}}, + watermarkBefore: watermarkFile{pkh: {"block": {Level: 6, Round: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 7, Round: 2, Hash: "vh2x7GpWVr8wSUE7HuAr5antYqgqzXFTDA4Wy6UUAMTzvNU1cnd4"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "signing duplicate request is ok", @@ -80,7 +77,7 @@ var functionalTestCases = []functionalTestCase{ expectedResponses: []any{SuccessResponse{Signature: "edsigu6FPqZdPhLNo5AGCkdscaiMdZXsiV5djxy1v2r89J1ZWfVfZ2UXJEcURSsx38JSHXtccr9o5yzJ46NL6mGEZbZ86fiJjuv"}, SuccessResponse{Signature: "edsigu6FPqZdPhLNo5AGCkdscaiMdZXsiV5djxy1v2r89J1ZWfVfZ2UXJEcURSsx38JSHXtccr9o5yzJ46NL6mGEZbZ86fiJjuv"}}, watermarkBefore: nil, - watermarkAfter: keyToWatermark{pkh: {Level: 2, Round: 0, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 2, Round: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "level is more significant than round for successful signing", @@ -90,8 +87,8 @@ var functionalTestCases = []functionalTestCase{ }, expectedStatusCodes: []int{200}, expectedResponses: []any{SuccessResponse{Signature: "edsigtbYeDN9n2VjpoCmPVt5tQBe6Y95wTPPpgtVTx3g2S4hQSwypcdBWm5s6gfTi567MFFpDqXPRmdUBo4VqseLZdZc8LWnvK9"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 6, Round: 3, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 7, Round: 2, Order: 0, Hash: "vh2x7GpWVr8wSUE7HuAr5antYqgqzXFTDA4Wy6UUAMTzvNU1cnd4"}}, + watermarkBefore: watermarkFile{pkh: {"block": {Level: 6, Round: 3, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 7, Round: 2, Hash: "vh2x7GpWVr8wSUE7HuAr5antYqgqzXFTDA4Wy6UUAMTzvNU1cnd4"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "level is more significant than round for identifying watermark", @@ -101,8 +98,8 @@ var functionalTestCases = []functionalTestCase{ }, expectedStatusCodes: []int{409}, expectedResponses: []any{[]FailureResponse{{Id: "failure", Kind: "temporary", Msg: "watermark validation failed"}}}, - watermarkBefore: keyToWatermark{pkh: {Level: 8, Round: 1, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 8, Round: 1, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, + watermarkBefore: watermarkFile{pkh: {"block": {Level: 8, Round: 1, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 8, Round: 1, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "round is used for watermark if level is the same", @@ -112,8 +109,8 @@ var functionalTestCases = []functionalTestCase{ }, expectedStatusCodes: []int{409}, expectedResponses: []any{[]FailureResponse{{Id: "failure", Kind: "temporary", Msg: "watermark validation failed"}}}, - watermarkBefore: keyToWatermark{pkh: {Level: 7, Round: 3, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 7, Round: 3, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, + watermarkBefore: watermarkFile{pkh: {"block": {Level: 7, Round: 3, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 7, Round: 3, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, chainID: "NetXo5iVw1vBoxM", }, {title: "baking happy path scenario - first sign block, then preendorsement, finally endorsement", @@ -127,76 +124,44 @@ var functionalTestCases = []functionalTestCase{ expectedResponses: []any{SuccessResponse{Signature: "edsigtfHHgiTKBhZ4wzfZwCtLpWsS1q4pHR47YbhWHLzxmx95LpSQQXXC4nWhoHCp7ppEeC2eHw2Be7zjQrKwHvsmb8KyJcGf1b"}, SuccessResponse{Signature: "edsigtjpdXbicHctbhx82tQKMYNFrwSimTkFj16iWo38jMSFB9gzxbyyAuKmX8detwLGp8CWDvHyFs5pcivpCSHrJVD4ZUWSb5r"}, SuccessResponse{Signature: "edsigtfoBKBjfYYASaf194oQknF3r5eag81ihkErSSi1WPiV6qfrQjCFWomAbjG63PKaLoUvoNxqK4TCD4MLoJAFC6JtHwtBYYn"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 33, Round: 2, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 2, Hash: "vh216VxjGyVK2XWEQ5gyFcAMLEqPzRKJijB6ZUybZjnwetdZp8Lm"}}, - chainID: "NetXo5iVw1vBoxM", - }, - {title: "baking operation block is order 0", - - signRequestBodies: []string{ - "\"11b3d79f99000000220181ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a000000006427822b0480a2ea07609835343e53282a2e0d8ef20f54d6a7c1603d3874c2a2a7fceb3d3d00000021000000010200000004000000220000000000000004fffffffd000000040000000415b3aa8c735353cf4cde8aec00319d8922538fef77d44ee6780c8872bf63408bfd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c0600000004a40d1a28000000000002\""}, - expectedStatusCodes: []int{200}, - expectedResponses: []any{SuccessResponse{Signature: "edsigtfHHgiTKBhZ4wzfZwCtLpWsS1q4pHR47YbhWHLzxmx95LpSQQXXC4nWhoHCp7ppEeC2eHw2Be7zjQrKwHvsmb8KyJcGf1b"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 33, Round: 2, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 0, Hash: "vh2E3QPN8R55XtdeZXsPtbgXErMeLcE5YbjDMcUzXsFByL97u5Qc"}}, - chainID: "NetXo5iVw1vBoxM", - }, - {title: "baking operation preendorsement is order 1", - - signRequestBodies: []string{ - "\"12b3d79f9981ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a1400040000002200000004fd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c06\""}, - expectedStatusCodes: []int{200}, - expectedResponses: []any{SuccessResponse{Signature: "edsigtjpdXbicHctbhx82tQKMYNFrwSimTkFj16iWo38jMSFB9gzxbyyAuKmX8detwLGp8CWDvHyFs5pcivpCSHrJVD4ZUWSb5r"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 0, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 1, Hash: "vh2KHE9afrJBSzLQcnP21cCtHfc9yPsjCVzsbBfLTpzRTenXtp1s"}}, - chainID: "NetXo5iVw1vBoxM", - }, - {title: "baking operation endorsement is order 2", - - signRequestBodies: []string{ - "\"13b3d79f9981ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a1500040000002200000004fd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c06\""}, - expectedStatusCodes: []int{200}, - expectedResponses: []any{SuccessResponse{Signature: "edsigtfoBKBjfYYASaf194oQknF3r5eag81ihkErSSi1WPiV6qfrQjCFWomAbjG63PKaLoUvoNxqK4TCD4MLoJAFC6JtHwtBYYn"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 1, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 2, Hash: "vh216VxjGyVK2XWEQ5gyFcAMLEqPzRKJijB6ZUybZjnwetdZp8Lm"}}, - chainID: "NetXo5iVw1vBoxM", + watermarkBefore: watermarkFile{pkh: {"block": {Level: 33, Round: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 34, Round: 4, Hash: "vh2E3QPN8R55XtdeZXsPtbgXErMeLcE5YbjDMcUzXsFByL97u5Qc"}, + "endorsement": {Level: 34, Round: 4, Hash: "vh216VxjGyVK2XWEQ5gyFcAMLEqPzRKJijB6ZUybZjnwetdZp8Lm"}, + "preendorsement": {Level: 34, Round: 4, Hash: "vh2KHE9afrJBSzLQcnP21cCtHfc9yPsjCVzsbBfLTpzRTenXtp1s"}}}, + chainID: "NetXo5iVw1vBoxM", }, - {title: "order is used to determine watermark", - - signRequestBodies: []string{ - "\"12b3d79f9981ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a1400040000002200000004fd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c06\"", - }, - expectedStatusCodes: []int{409}, - expectedResponses: []any{[]FailureResponse{{Id: "failure", Kind: "temporary", Msg: "watermark validation failed"}}}, - watermarkBefore: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - chainID: "NetXo5iVw1vBoxM", - }, - {title: "round trumps order", + {title: "baking scenario - block request can arrive last", signRequestBodies: []string{ "\"12b3d79f9981ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a1400040000002200000004fd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c06\"", + "\"13b3d79f9981ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a1500040000002200000004fd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c06\"", + "\"11b3d79f99000000220181ff3a903959741b102d579148d3b1ea56ad7bc77e102def5cc73c49c062217a000000006427822b0480a2ea07609835343e53282a2e0d8ef20f54d6a7c1603d3874c2a2a7fceb3d3d00000021000000010200000004000000220000000000000004fffffffd000000040000000415b3aa8c735353cf4cde8aec00319d8922538fef77d44ee6780c8872bf63408bfd5a576ce08fa0e87e7b41d373d04a2df8798b234c7a4f8483a1d839e0066c0600000004a40d1a28000000000002\"", }, - expectedStatusCodes: []int{200}, - expectedResponses: []any{SuccessResponse{Signature: "edsigtjpdXbicHctbhx82tQKMYNFrwSimTkFj16iWo38jMSFB9gzxbyyAuKmX8detwLGp8CWDvHyFs5pcivpCSHrJVD4ZUWSb5r"}}, - watermarkBefore: keyToWatermark{pkh: {Level: 34, Round: 3, Order: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}, - watermarkAfter: keyToWatermark{pkh: {Level: 34, Round: 4, Order: 1, Hash: "vh2KHE9afrJBSzLQcnP21cCtHfc9yPsjCVzsbBfLTpzRTenXtp1s"}}, - chainID: "NetXo5iVw1vBoxM", + expectedStatusCodes: []int{200, 200, 200}, + expectedResponses: []any{SuccessResponse{Signature: "edsigtjpdXbicHctbhx82tQKMYNFrwSimTkFj16iWo38jMSFB9gzxbyyAuKmX8detwLGp8CWDvHyFs5pcivpCSHrJVD4ZUWSb5r"}, + SuccessResponse{Signature: "edsigtfoBKBjfYYASaf194oQknF3r5eag81ihkErSSi1WPiV6qfrQjCFWomAbjG63PKaLoUvoNxqK4TCD4MLoJAFC6JtHwtBYYn"}, + SuccessResponse{Signature: "edsigtfHHgiTKBhZ4wzfZwCtLpWsS1q4pHR47YbhWHLzxmx95LpSQQXXC4nWhoHCp7ppEeC2eHw2Be7zjQrKwHvsmb8KyJcGf1b"}}, + watermarkBefore: watermarkFile{pkh: {"block": {Level: 33, Round: 2, Hash: "vh2i6wpkn9bGpw47r3RhnfHiCrLvzTvQT36AmEZNPPsKqcN7yecT"}}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 34, Round: 4, Hash: "vh2E3QPN8R55XtdeZXsPtbgXErMeLcE5YbjDMcUzXsFByL97u5Qc"}, + "endorsement": {Level: 34, Round: 4, Hash: "vh216VxjGyVK2XWEQ5gyFcAMLEqPzRKJijB6ZUybZjnwetdZp8Lm"}, + "preendorsement": {Level: 34, Round: 4, Hash: "vh2KHE9afrJBSzLQcnP21cCtHfc9yPsjCVzsbBfLTpzRTenXtp1s"}}}, + chainID: "NetXo5iVw1vBoxM", }, } func TestWatermark(t *testing.T) { - remove_watermark_files() for _, test := range functionalTestCases { + remove_watermark_files() + restart_signatory() t.Run(test.title, func(t *testing.T) { if test.watermarkBefore != nil { mkdir() write_watermark_file(test.watermarkBefore, test.chainID+".json") + restart_signatory() } - defer remove_watermark_files() for i, request := range test.signRequestBodies { code, message := request_sign(request) - assert.Equal(t, test.expectedStatusCodes[i], code) + require.Equal(t, test.expectedStatusCodes[i], code) if code == 200 { var sr SuccessResponse dec := json.NewDecoder(bytes.NewReader(message)) @@ -212,13 +177,15 @@ func TestWatermark(t *testing.T) { } } b := read_watermark_file(test.chainID) - var ktw keyToWatermark + var wf watermarkFile dec := json.NewDecoder(bytes.NewReader(b)) - err := dec.Decode(&ktw) + err := dec.Decode(&wf) require.Nil(t, err) - assert.Equal(t, test.watermarkAfter, ktw) + assert.Equal(t, test.watermarkAfter, wf) }) } + remove_watermark_files() + restart_signatory() } type concurrencyTestCase struct { @@ -229,8 +196,8 @@ type concurrencyTestCase struct { expectedFailureCount int expectedFailureCode int expectedFailureMessage string - watermarkBefore keyToWatermark - watermarkAfter keyToWatermark + watermarkBefore watermarkFile + watermarkAfter watermarkFile chainID string } @@ -254,7 +221,7 @@ var concurrencyTestCases = []concurrencyTestCase{ expectedFailureMessage: "watermark validation failed", expectedFailureCode: 409, watermarkBefore: nil, - watermarkAfter: keyToWatermark{pkh: {Level: 2, Round: 0, Order: 0, Hash: "unknown_not_verfied"}}, + watermarkAfter: watermarkFile{pkh: {"block": {Level: 2, Round: 0, Hash: "unknown_not_verfied"}}}, chainID: "NetXo5iVw1vBoxM", }, } @@ -266,13 +233,15 @@ var ( codes []int ) -func TestConcurrency(t *testing.T) { +func TestWatermarkConcurrency(t *testing.T) { for _, test := range concurrencyTestCases { remove_watermark_files() + restart_signatory() t.Run(test.title, func(t *testing.T) { mkdir() if test.watermarkBefore != nil { write_watermark_file(test.watermarkBefore, test.chainID+".json") + restart_signatory() } n := len(test.signRequestBodies) wg.Add(n) @@ -295,15 +264,16 @@ func TestConcurrency(t *testing.T) { require.Equal(t, test.expectedSuccessCount, success) require.Equal(t, test.expectedFailureCount, fail) b := read_watermark_file(test.chainID) - var ktw keyToWatermark + var wf watermarkFile dec := json.NewDecoder(bytes.NewReader(b)) - err := dec.Decode(&ktw) + err := dec.Decode(&wf) require.Nil(t, err) - require.Equal(t, test.watermarkAfter[pkh].Level, ktw[pkh].Level) - require.Equal(t, test.watermarkAfter[pkh].Round, ktw[pkh].Round) - require.Equal(t, test.watermarkAfter[pkh].Order, ktw[pkh].Order) + require.Equal(t, test.watermarkAfter[pkh]["block"].Level, wf[pkh]["block"].Level) + require.Equal(t, test.watermarkAfter[pkh]["block"].Round, wf[pkh]["block"].Round) }) } + remove_watermark_files() + restart_signatory() } func request_sign_concurrent(request string) { @@ -331,8 +301,8 @@ func remove_watermark_files() { } } -func write_watermark_file(ktw keyToWatermark, filename string) { - json, err := json.Marshal(ktw) +func write_watermark_file(wf watermarkFile, filename string) { + json, err := json.Marshal(wf) if err != nil { panic("json marshal failed") } @@ -367,6 +337,5 @@ func request_sign(body string) (int, []byte) { if err != nil { panic(err) } - //fmt.Println(string(bytes)) return resp.StatusCode, bytes } diff --git a/pkg/signatory/policy_hook_test.go b/pkg/signatory/policy_hook_test.go index 175142f3..014004d0 100644 --- a/pkg/signatory/policy_hook_test.go +++ b/pkg/signatory/policy_hook_test.go @@ -119,7 +119,7 @@ func testPolicyHookAuth(t *testing.T, status int) error { s, err := signatory.New(context.Background(), &conf) require.NoError(t, err) - msg := mustHex("019caecab9000753d3029bc7d9a36b60cce68ade985a0a16929587166e0d3de61efff2fa31b7116bf670000000005ee3c23b04519d71c4e54089c56773c44979b3ba3d61078ade40332ad81577ae074f653e0e0000001100000001010000000800000000000753d2da051ba81185783e4cbc633cf2ba809139ef07c3e5f6c5867f930e7667b224430000cde7fbbb948e030000") + msg := mustHex("11ed9d217c0000518e0118425847ac255b6d7c30ce8fec23b8eaf13b741de7d18509ac2ef83c741209630000000061947af504805682ea5d089837764b3efcc90b91db24294ff9ddb66019f332ccba17cc4741000000210000000102000000040000518e0000000000000004ffffffff0000000400000000eb1320a71e8bf8b0162a3ec315461e9153a38b70d00d5dde2df85eb92748f8d068d776e356683a9e23c186ccfb72ddc6c9857bb1704487972922e7c89a7121f800000000a8e1dd3c000000000000") _, err = s.Sign(context.Background(), &signatory.SignRequest{PublicKeyHash: signKeyHash, Message: msg}) return err } @@ -156,7 +156,7 @@ func testPolicyHook(t *testing.T, status int) error { s, err := signatory.New(context.Background(), &conf) require.NoError(t, err) - msg := mustHex("019caecab9000753d3029bc7d9a36b60cce68ade985a0a16929587166e0d3de61efff2fa31b7116bf670000000005ee3c23b04519d71c4e54089c56773c44979b3ba3d61078ade40332ad81577ae074f653e0e0000001100000001010000000800000000000753d2da051ba81185783e4cbc633cf2ba809139ef07c3e5f6c5867f930e7667b224430000cde7fbbb948e030000") + msg := mustHex("11ed9d217c0000518e0118425847ac255b6d7c30ce8fec23b8eaf13b741de7d18509ac2ef83c741209630000000061947af504805682ea5d089837764b3efcc90b91db24294ff9ddb66019f332ccba17cc4741000000210000000102000000040000518e0000000000000004ffffffff0000000400000000eb1320a71e8bf8b0162a3ec315461e9153a38b70d00d5dde2df85eb92748f8d068d776e356683a9e23c186ccfb72ddc6c9857bb1704487972922e7c89a7121f800000000a8e1dd3c000000000000") _, err = s.Sign(context.Background(), &signatory.SignRequest{PublicKeyHash: signKeyHash, Message: msg}) return err } diff --git a/pkg/signatory/request/request.go b/pkg/signatory/request/request.go index 15ca2b5f..298f5d99 100644 --- a/pkg/signatory/request/request.go +++ b/pkg/signatory/request/request.go @@ -11,27 +11,15 @@ type SignRequest interface { RequestKind() string } -type EmmyBlockRequest struct { - Chain *tz.ChainID - BlockHeader protocol.ShellHeader -} - -func (*EmmyBlockRequest) RequestKind() string { return "block" } - -type TenderbakeBlockRequest struct { +type BlockRequest struct { Chain *tz.ChainID BlockHeader protocol.TenderbakeBlockHeader } -func (*TenderbakeBlockRequest) RequestKind() string { return "block" } - -type EmmyEndorsementRequest struct { - Chain *tz.ChainID - Branch *tz.BlockHash - Operation protocol.InlinedEmmyEndorsementContents -} - -func (*EmmyEndorsementRequest) RequestKind() string { return "endorsement" } +func (*BlockRequest) RequestKind() string { return "block" } +func (r *BlockRequest) GetChainID() *tz.ChainID { return r.Chain } +func (r *BlockRequest) GetLevel() int32 { return r.BlockHeader.Level } +func (r *BlockRequest) GetRound() int32 { return r.BlockHeader.PayloadRound } type PreendorsementRequest struct { Chain *tz.ChainID @@ -39,7 +27,10 @@ type PreendorsementRequest struct { Operation protocol.InlinedPreendorsementContents } -func (*PreendorsementRequest) RequestKind() string { return "preendorsement" } +func (*PreendorsementRequest) RequestKind() string { return "preendorsement" } +func (r *PreendorsementRequest) GetChainID() *tz.ChainID { return r.Chain } +func (r *PreendorsementRequest) GetLevel() int32 { return r.Operation.(*protocol.Preendorsement).Level } +func (r *PreendorsementRequest) GetRound() int32 { return r.Operation.(*protocol.Preendorsement).Round } type EndorsementRequest struct { Chain *tz.ChainID @@ -47,7 +38,10 @@ type EndorsementRequest struct { Operation protocol.InlinedEndorsementContents } -func (*EndorsementRequest) RequestKind() string { return "endorsement" } +func (*EndorsementRequest) RequestKind() string { return "endorsement" } +func (r *EndorsementRequest) GetChainID() *tz.ChainID { return r.Chain } +func (r *EndorsementRequest) GetLevel() int32 { return r.Operation.(*protocol.Endorsement).Level } +func (r *EndorsementRequest) GetRound() int32 { return r.Operation.(*protocol.Endorsement).Round } type GenericOperationRequest struct { Branch *tz.BlockHash @@ -59,10 +53,8 @@ func (*GenericOperationRequest) RequestKind() string { return "generic" } func init() { encoding.RegisterEnum(&encoding.Enum[SignRequest]{ Variants: encoding.Variants[SignRequest]{ - 0x01: (*EmmyBlockRequest)(nil), - 0x02: (*EmmyEndorsementRequest)(nil), 0x03: (*GenericOperationRequest)(nil), - 0x11: (*TenderbakeBlockRequest)(nil), + 0x11: (*BlockRequest)(nil), 0x12: (*PreendorsementRequest)(nil), 0x13: (*EndorsementRequest)(nil), }, @@ -71,131 +63,40 @@ func init() { type WithWatermark interface { SignRequest - Watermark() *Watermark -} - -const ( - WmOrderDefault = iota - WmOrderPreendorsement - WmOrderEndorsement -) - -type Level struct { - Level int32 `json:"level"` - Round tz.Option[int32] `json:"round"` -} - -func (l *Level) Cmp(other *Level) tz.Option[int] { - if l.Round.IsNone() && other.Round.IsSome() { - return tz.None[int]() - } - - if d := l.Level - other.Level; d == 0 { - switch { - case l.Round.IsSome() && other.Round.IsSome(): - return tz.Some(int(l.Round.Unwrap() - other.Round.Unwrap())) - case l.Round.IsSome() && other.Round.IsNone(): - return tz.Some(1) - default: - return tz.Some(0) - } - } else { - return tz.Some(int(d)) - } + GetChainID() *tz.ChainID + GetLevel() int32 + GetRound() int32 } type Watermark struct { - Level - Chain *tz.ChainID - Order int -} - -type StoredWatermark struct { - Level - Order int `json:"order"` + Level int32 `json:"level"` + Round int32 `json:"round"` Hash tz.Option[tz.BlockPayloadHash] `json:"hash"` } -func (w *Watermark) Stored(hash *crypt.Digest) *StoredWatermark { - wm := StoredWatermark{ - Level: w.Level, - Order: w.Order, - } - if hash != nil { - var h tz.BlockPayloadHash - copy(h[:], hash[:]) - wm.Hash = tz.Some(h) - } - return &wm -} - -func (w *Watermark) Validate(stored *StoredWatermark, hash *crypt.Digest) bool { - if hash != nil && stored.Hash.IsSome() && *(*tz.BlockPayloadHash)(hash) == stored.Hash.Unwrap() { - return true - } - c := w.Level.Cmp(&stored.Level) - return c.IsSome() && (c.Unwrap() > 0 || c.Unwrap() == 0 && w.Order > stored.Order) -} - -func (r *EmmyBlockRequest) Watermark() *Watermark { - return &Watermark{ - Chain: r.Chain, - Level: Level{ - Level: r.BlockHeader.Level, - Round: tz.None[int32](), - }, - Order: WmOrderDefault, - } -} - -func (r *TenderbakeBlockRequest) Watermark() *Watermark { - return &Watermark{ - Chain: r.Chain, - Level: Level{ - Level: r.BlockHeader.Level, - Round: tz.Some(r.BlockHeader.PayloadRound), - }, - Order: WmOrderDefault, - } -} - -func (r *EmmyEndorsementRequest) Watermark() *Watermark { +func NewWatermark(req WithWatermark, hash *crypt.Digest) *Watermark { return &Watermark{ - Chain: r.Chain, - Level: Level{ - Level: r.Operation.(*protocol.EmmyEndorsement).Level, - Round: tz.None[int32](), - }, - Order: WmOrderEndorsement, + Level: req.GetLevel(), + Round: req.GetRound(), + Hash: tz.Some((tz.BlockPayloadHash)(*hash)), } } -func (r *PreendorsementRequest) Watermark() *Watermark { - return &Watermark{ - Chain: r.Chain, - Level: Level{ - Level: r.Operation.(*protocol.Preendorsement).Level, - Round: tz.Some(r.Operation.(*protocol.Preendorsement).Round), - }, - Order: WmOrderPreendorsement, +func (l *Watermark) Validate(stored *Watermark) bool { + if l.Hash.IsSome() && stored.Hash.IsSome() && l.Hash.Unwrap() == stored.Hash.Unwrap() { + return true } -} - -func (r *EndorsementRequest) Watermark() *Watermark { - return &Watermark{ - Chain: r.Chain, - Level: Level{ - Level: r.Operation.(*protocol.Endorsement).Level, - Round: tz.Some(r.Operation.(*protocol.Endorsement).Round), - }, - Order: WmOrderEndorsement, + var diff int32 + if d := l.Level - stored.Level; d == 0 { + diff = l.Round - stored.Round + } else { + diff = d } + return diff > 0 } var ( - _ WithWatermark = (*EmmyBlockRequest)(nil) - _ WithWatermark = (*EmmyEndorsementRequest)(nil) - _ WithWatermark = (*TenderbakeBlockRequest)(nil) + _ WithWatermark = (*BlockRequest)(nil) _ WithWatermark = (*PreendorsementRequest)(nil) _ WithWatermark = (*EndorsementRequest)(nil) ) diff --git a/pkg/signatory/request/request_test.go b/pkg/signatory/request/request_test.go index 14d4a77f..4b01cb6b 100644 --- a/pkg/signatory/request/request_test.go +++ b/pkg/signatory/request/request_test.go @@ -68,7 +68,7 @@ func TestSignRequest(t *testing.T) { { title: "block", src: "11ed9d217c0000518e0118425847ac255b6d7c30ce8fec23b8eaf13b741de7d18509ac2ef83c741209630000000061947af504805682ea5d089837764b3efcc90b91db24294ff9ddb66019f332ccba17cc4741000000210000000102000000040000518e0000000000000004ffffffff0000000400000000eb1320a71e8bf8b0162a3ec315461e9153a38b70d00d5dde2df85eb92748f8d068d776e356683a9e23c186ccfb72ddc6c9857bb1704487972922e7c89a7121f800000000a8e1dd3c000000000000", - expect: &TenderbakeBlockRequest{ + expect: &BlockRequest{ Chain: &tz.ChainID{0xed, 0x9d, 0x21, 0x7c}, BlockHeader: proto.TenderbakeBlockHeader{ ShellHeader: proto.ShellHeader{ diff --git a/pkg/signatory/request/watermark_test.go b/pkg/signatory/request/watermark_test.go index a3cbaa7e..373cc322 100644 --- a/pkg/signatory/request/watermark_test.go +++ b/pkg/signatory/request/watermark_test.go @@ -10,282 +10,131 @@ import ( "github.com/stretchr/testify/require" ) +type dummyMsg struct { + level int32 + round int32 +} + +func (r *dummyMsg) RequestKind() string { return "dummy" } +func (r *dummyMsg) GetChainID() *tz.ChainID { return &tz.ChainID{} } +func (r *dummyMsg) GetLevel() int32 { return r.level } +func (r *dummyMsg) GetRound() int32 { return r.round } + func TestWatermark(t *testing.T) { type expect struct { - wm Watermark + req WithWatermark digest *crypt.Digest expect bool } type testCase struct { - stored StoredWatermark + stored Watermark expect []expect } testCases := []testCase{ { - stored: StoredWatermark{ - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderDefault, + stored: Watermark{ + Level: 1, + Round: 1, }, expect: []expect{ { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(0)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 2, + round: 0, }, digest: &crypt.Digest{0}, expect: true, // level above }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(0)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 2, }, digest: &crypt.Digest{0}, - expect: true, // repeat - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.None[int32](), - }, - Order: WmOrderDefault, - }, - expect: false, // round is set above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, - }, - expect: true, // level and round above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, - }, expect: true, // round above }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 0, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 1, }, - expect: false, // level below + digest: &crypt.Digest{0}, + expect: false, // repeated }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 0, }, - expect: false, // level and round below + digest: &crypt.Digest{0}, + expect: false, // round below }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderEndorsement, + req: &dummyMsg{ + level: 0, + round: 2, }, - expect: true, // don't have endorsement + digest: &crypt.Digest{0}, + expect: false, // level below }, }, }, { - stored: StoredWatermark{ - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderEndorsement, + stored: Watermark{ + Level: 1, + Round: 1, + Hash: tz.Some(tz.BlockPayloadHash{0}), }, expect: []expect{ { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(0)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 2, + round: 0, }, + digest: &crypt.Digest{1}, expect: true, // level above }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(0)), - }, - Order: WmOrderEndorsement, - }, - expect: true, // level above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, - }, - expect: true, // level and round above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(2)), - }, - Order: WmOrderEndorsement, - }, - expect: true, // level and round above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 2, }, + digest: &crypt.Digest{1}, expect: true, // round above }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(2)), - }, - Order: WmOrderEndorsement, + req: &dummyMsg{ + level: 1, + round: 1, }, - expect: true, // order above + digest: &crypt.Digest{0}, + expect: true, // hash match }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 0, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 0, }, - expect: false, // level below + digest: &crypt.Digest{1}, + expect: false, // round below }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 0, - Round: tz.Some(int32(2)), - }, - Order: WmOrderEndorsement, + req: &dummyMsg{ + level: 0, + round: 2, }, + digest: &crypt.Digest{1}, expect: false, // level below }, { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderDefault, - }, - expect: false, // level and round below - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 1, - Round: tz.Some(int32(1)), - }, - Order: WmOrderEndorsement, - }, - expect: false, // have endorsement - }, - }, - }, - { - stored: StoredWatermark{ - Level: Level{ - Level: 1, - Round: tz.None[int32](), - }, - Order: WmOrderDefault, - }, - expect: []expect{ - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.None[int32](), - }, - Order: WmOrderDefault, + req: &dummyMsg{ + level: 1, + round: 1, }, - expect: true, // level above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 2, - Round: tz.Some(int32(2)), - }, - Order: WmOrderDefault, - }, - expect: true, // level above - }, - { - wm: Watermark{ - Chain: &tz.ChainID{}, - Level: Level{ - Level: 0, - Round: tz.None[int32](), - }, - Order: WmOrderDefault, - }, - expect: false, // level below + digest: &crypt.Digest{1}, + expect: false, // repeated }, }, }, @@ -293,7 +142,8 @@ func TestWatermark(t *testing.T) { for _, c := range testCases { for _, ex := range c.expect { - require.Equal(t, ex.expect, ex.wm.Validate(&c.stored, ex.digest)) + wm := NewWatermark(ex.req, ex.digest) + require.Equal(t, ex.expect, wm.Validate(&c.stored)) } } } diff --git a/pkg/signatory/signatory.go b/pkg/signatory/signatory.go index 0c9e1fc1..2be7a33d 100644 --- a/pkg/signatory/signatory.go +++ b/pkg/signatory/signatory.go @@ -349,8 +349,7 @@ func (s *Signatory) Sign(ctx context.Context, req *SignRequest) (crypt.Signature l = l.WithField(logReq, msg.RequestKind()) if m, ok := msg.(request.WithWatermark); ok { - wm := m.Watermark() - l = l.WithFields(log.Fields{logChainID: string(wm.Chain.ToBase58()), logLevel: wm.Level.Level}) + l = l.WithFields(log.Fields{logChainID: string(m.GetChainID().ToBase58()), logLevel: m.GetLevel()}) } var opStat operationsStat diff --git a/pkg/signatory/signatory_test.go b/pkg/signatory/signatory_test.go index 05de151b..2204a9f6 100644 --- a/pkg/signatory/signatory_test.go +++ b/pkg/signatory/signatory_test.go @@ -74,7 +74,7 @@ func TestPolicy(t *testing.T) { }, { title: "block not allowed", - msg: mustHex("019caecab9000753d3029bc7d9a36b60cce68ade985a0a16929587166e0d3de61efff2fa31b7116bf670000000005ee3c23b04519d71c4e54089c56773c44979b3ba3d61078ade40332ad81577ae074f653e0e0000001100000001010000000800000000000753d2da051ba81185783e4cbc633cf2ba809139ef07c3e5f6c5867f930e7667b224430000cde7fbbb948e030000"), + msg: mustHex("11ed9d217c0000518e0118425847ac255b6d7c30ce8fec23b8eaf13b741de7d18509ac2ef83c741209630000000061947af504805682ea5d089837764b3efcc90b91db24294ff9ddb66019f332ccba17cc4741000000210000000102000000040000518e0000000000000004ffffffff0000000400000000eb1320a71e8bf8b0162a3ec315461e9153a38b70d00d5dde2df85eb92748f8d068d776e356683a9e23c186ccfb72ddc6c9857bb1704487972922e7c89a7121f800000000a8e1dd3c000000000000"), policy: signatory.PublicKeyPolicy{ AllowedRequests: []string{"generic", "endorsement"}, AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"}, @@ -84,7 +84,7 @@ func TestPolicy(t *testing.T) { }, { title: "endorsement ok", - msg: mustHex("029caecab9e3c579180719b76b585cbdf7e440914b8e09fc0e8c64a26b7a4eacd545ad653100000753c3"), + msg: mustHex("13ed9d217cfc81eee810737b04018acef4db74d056b79edc43e6be46cae7e4c217c22a82f01500120000518d0000000003e7ea1f67dbb0bb6cfa372cb092cd9cf786b4f1b5e5139da95b915fb95e698d"), policy: signatory.PublicKeyPolicy{ AllowedRequests: []string{"generic", "block", "endorsement"}, AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"}, @@ -93,7 +93,7 @@ func TestPolicy(t *testing.T) { }, { title: "endorsement not allowed", - msg: mustHex("029caecab9e3c579180719b76b585cbdf7e440914b8e09fc0e8c64a26b7a4eacd545ad653100000753c3"), + msg: mustHex("13ed9d217cfc81eee810737b04018acef4db74d056b79edc43e6be46cae7e4c217c22a82f01500120000518d0000000003e7ea1f67dbb0bb6cfa372cb092cd9cf786b4f1b5e5139da95b915fb95e698d"), policy: signatory.PublicKeyPolicy{ AllowedRequests: []string{"generic", "block"}, AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"}, @@ -103,7 +103,7 @@ func TestPolicy(t *testing.T) { }, { title: "generic ok", - msg: mustHex("019caecab900061de402e27da655a04eaa5dad0647e6ff56d11a5da8efb48c2e90570e27853839e76b68000000005eb576f5047ab08836902391c075dc92640f9d7496faa8cff5b2b24450786d86349b9a528d000000110000000101000000080000000000061de3ad348c90c42bc5b90e89837fdbeb6c1360be7181c9116ef2eb8cb63ebbb1380e00000675d7e0ffa2030000"), + msg: mustHex("03a60703a9567bf69ec66b368c3d8562eba4cbf29278c2c10447a684e3aa1436856c00a0c7a9b0bcd6a48ee0c13094327f215ba2adeaa7d40dabc1af25e36fde02c096b10201f525eabd8b0eeace1494233ea0230d2c9ad6619b00ffff0b66756c66696c6c5f61736b0000000907070088f0f6010306"), policy: signatory.PublicKeyPolicy{ AllowedRequests: []string{"generic", "block", "endorsement"}, AllowedOps: []string{"endorsement", "seed_nonce_revelation", "activate_account", "ballot", "reveal", "transaction", "origination", "delegation"}, diff --git a/pkg/signatory/watermark_file.go b/pkg/signatory/watermark_file.go index a8c86b9c..ab9d82aa 100644 --- a/pkg/signatory/watermark_file.go +++ b/pkg/signatory/watermark_file.go @@ -8,89 +8,131 @@ import ( "io/fs" "os" "path/filepath" - "sync" + "strings" tz "github.com/ecadlabs/gotez" + "github.com/ecadlabs/gotez/b58" "github.com/ecadlabs/signatory/pkg/crypt" "github.com/ecadlabs/signatory/pkg/hashmap" "github.com/ecadlabs/signatory/pkg/signatory/request" log "github.com/sirupsen/logrus" ) -// chain -> delegate(pkh) -type delegateMap = hashmap.PublicKeyHashMap[*request.StoredWatermark] -type chainMap map[tz.ChainID]delegateMap - -var ErrWatermark = errors.New("watermark validation failed") - -const watermarkDir = "watermark_v1" - type FileWatermark struct { - BaseDir string - mtx sync.Mutex + baseDir string + mem InMemoryWatermark } -type legacyWatermarkData struct { - Round int32 `json:"round,omitempty"` - Level int32 `json:"level"` - Hash tz.Option[tz.BlockPayloadHash] `json:"hash"` -} +// chain -> delegate(pkh) -> request type -> watermark +type delegateMap = hashmap.PublicKeyHashMap[requestMap] +type requestMap = map[string]*request.Watermark -type legacyKindMap map[string]legacyWatermarkMap -type legacyWatermarkMap = hashmap.PublicKeyHashMap[*legacyWatermarkData] +var ErrWatermark = errors.New("watermark validation failed") -const legacyWatermarkDir = "watermark" +const watermarkDir = "watermark_v2" -func (f *FileWatermark) tryLegacy(filename string) (delegateMap, error) { - var kinds legacyKindMap - fd, err := os.Open(filename) - if err == nil { - err = json.NewDecoder(fd).Decode(&kinds) - fd.Close() - if err != nil { +func tryLoad(baseDir string) (map[tz.ChainID]delegateMap, error) { + dir := filepath.Join(baseDir, watermarkDir) + entries, err := os.ReadDir(dir) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { return nil, err } - } else if !errors.Is(err, fs.ErrNotExist) { - return nil, err - } else { return nil, nil } - orders := []struct { - kind string - order int - }{ - {"endorsement", request.WmOrderEndorsement}, - {"preendorsement", request.WmOrderPreendorsement}, - {"block", request.WmOrderDefault}, - {"generic", request.WmOrderDefault}, + out := make(map[tz.ChainID]delegateMap) + for _, ent := range entries { + if !ent.Type().IsRegular() || !strings.HasSuffix(ent.Name(), ".json") { + continue + } + name := ent.Name() + chainID, err := b58.ParseChainID([]byte(name[:len(name)-5])) + if err != nil { + return nil, err + } + + filename := filepath.Join(dir, name) + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + var delegates delegateMap + if err = json.NewDecoder(fd).Decode(&delegates); err != nil { + return nil, err + } + out[*chainID] = delegates } - out := make(delegateMap) - for _, o := range orders { - if wm, ok := kinds[o.kind]; ok { - wm.ForEach(func(pkh tz.PublicKeyHash, data *legacyWatermarkData) bool { - stored := request.StoredWatermark{ - Level: request.Level{ - Level: data.Level, - Round: tz.Some(data.Round), - }, - Order: o.order, - Hash: data.Hash, - } - if s, ok := out.Get(pkh); !ok || ok && s.Order <= o.order { - out.Insert(pkh, &stored) + return out, nil +} + +func NewFileWatermark(baseDir string) (*FileWatermark, error) { + wm := FileWatermark{ + baseDir: baseDir, + } + var err error + if wm.mem.chains, err = tryLoad(baseDir); err != nil { + return nil, err + } + if wm.mem.chains != nil { + // load ok, give a warning if legasy data still exist + if ok, err := checkV0exist(baseDir); err == nil { + if ok { + log.Warnf("Watermark storage directory %s is deprecated and must be removed manually", v0WatermarkDir) + } + } else { + return nil, err + } + if ok, err := checkV1exist(baseDir); err == nil { + if ok { + log.Warnf("Watermark storage directory %s is deprecated and must be removed manually", v1WatermarkDir) + } + } else { + return nil, err + } + } else { + // do migration + if wm.mem.chains, err = tryV1(baseDir); err != nil { + return nil, err + } + if wm.mem.chains != nil { + if err = writeAll(baseDir, wm.mem.chains); err != nil { + return nil, err + } + log.Infof("Watermark data migrated successfully to %s. Old watermark storage directory %s can now be safely removed", watermarkDir, v1WatermarkDir) + } else { + if wm.mem.chains, err = tryV0(baseDir); err != nil { + return nil, err + } + if wm.mem.chains != nil { + if err = writeAll(baseDir, wm.mem.chains); err != nil { + return nil, err } - return true - }) + log.Infof("Watermark data migrated successfully to %s. Old watermark storage directory %s can now be safely removed", watermarkDir, v0WatermarkDir) + } } } + return &wm, nil +} - return out, nil +func writeAll(baseDir string, chains map[tz.ChainID]delegateMap) error { + for chain, data := range chains { + if err := writeWatermarkData(baseDir, data, &chain); err != nil { + return err + } + } + return nil } -func writeWatermarkData(data delegateMap, filename string) error { - fd, err := os.Create(filename) +func writeWatermarkData(baseDir string, data delegateMap, chain *tz.ChainID) error { + dir := filepath.Join(baseDir, watermarkDir) + if err := os.MkdirAll(dir, 0770); err != nil { + return err + } + + fd, err := os.Create(filepath.Join(dir, fmt.Sprintf("%s.json", chain.String()))) if err != nil { return err } @@ -110,52 +152,14 @@ func (f *FileWatermark) IsSafeToSign(pkh crypt.PublicKeyHash, req request.SignRe // watermark is not required return nil } - watermark := m.Watermark() - - dir := filepath.Join(f.BaseDir, watermarkDir) - if err := os.MkdirAll(dir, 0770); err != nil { - return err - } - filename := filepath.Join(dir, fmt.Sprintf("%s.json", watermark.Chain.String())) - legacyFilename := filepath.Join(f.BaseDir, legacyWatermarkDir, fmt.Sprintf("%s.json", watermark.Chain.String())) + f.mem.mtx.Lock() + defer f.mem.mtx.Unlock() - f.mtx.Lock() - defer f.mtx.Unlock() - - var delegates delegateMap - fd, err := os.Open(filename) - if err == nil { - err = json.NewDecoder(fd).Decode(&delegates) - fd.Close() - if err != nil { - return err - } - if legacy, err := f.tryLegacy(legacyFilename); err != nil { - return err - } else if legacy != nil { - log.Warnf("Watermark storage directory %s is deprecated and must be removed manually", legacyWatermarkDir) - } - } else if !errors.Is(err, fs.ErrNotExist) { + if err := f.mem.isSafeToSignUnlocked(pkh, m, digest); err != nil { return err - } else if delegates, err = f.tryLegacy(legacyFilename); err != nil { - return err - } else if delegates != nil { - // successful migration - if err := writeWatermarkData(delegates, filename); err != nil { - return err - } - log.Infof("Watermark data migrated successfully to %s. Old watermark storage directory %s can now be safely removed", watermarkDir, legacyWatermarkDir) - } else { - delegates = make(delegateMap) - } - - if wm, ok := delegates.Get(pkh); ok { - if !watermark.Validate(wm, digest) { - return ErrWatermark - } } - delegates.Insert(pkh, watermark.Stored(digest)) - return writeWatermarkData(delegates, filename) + chain := m.GetChainID() + return writeWatermarkData(f.baseDir, f.mem.chains[*chain], chain) } var _ Watermark = (*FileWatermark)(nil) diff --git a/pkg/signatory/watermark_mem.go b/pkg/signatory/watermark_mem.go index 8a2f1825..c2dd8578 100644 --- a/pkg/signatory/watermark_mem.go +++ b/pkg/signatory/watermark_mem.go @@ -3,45 +3,54 @@ package signatory import ( "sync" + tz "github.com/ecadlabs/gotez" "github.com/ecadlabs/signatory/pkg/crypt" "github.com/ecadlabs/signatory/pkg/signatory/request" ) // InMemoryWatermark keep previous operation in memory type InMemoryWatermark struct { - chains chainMap + chains map[tz.ChainID]delegateMap mtx sync.Mutex } // IsSafeToSign return true if this msgID is safe to sign func (w *InMemoryWatermark) IsSafeToSign(pkh crypt.PublicKeyHash, req request.SignRequest, digest *crypt.Digest) error { + w.mtx.Lock() + defer w.mtx.Unlock() + return w.isSafeToSignUnlocked(pkh, req, digest) +} + +func (w *InMemoryWatermark) isSafeToSignUnlocked(pkh crypt.PublicKeyHash, req request.SignRequest, digest *crypt.Digest) error { m, ok := req.(request.WithWatermark) if !ok { // watermark is not required return nil } - watermark := m.Watermark() - - w.mtx.Lock() - defer w.mtx.Unlock() if w.chains == nil { - w.chains = make(chainMap) + w.chains = make(map[tz.ChainID]delegateMap) } - delegates, ok := w.chains[*watermark.Chain] - if ok { - if wm, ok := delegates.Get(pkh); ok { - if !watermark.Validate(wm, digest) { - return ErrWatermark - } - } - } else { + delegates, ok := w.chains[*m.GetChainID()] + if !ok { delegates = make(delegateMap) - w.chains[*watermark.Chain] = delegates + w.chains[*m.GetChainID()] = delegates } - delegates.Insert(pkh, watermark.Stored(digest)) + requests, ok := delegates.Get(pkh) + if !ok { + requests = make(requestMap) + delegates.Insert(pkh, requests) + } + + watermark := request.NewWatermark(m, digest) + if stored, ok := requests[req.RequestKind()]; ok { + if !watermark.Validate(stored) { + return ErrWatermark + } + } + requests[m.RequestKind()] = watermark return nil } diff --git a/pkg/signatory/watermark_migration.go b/pkg/signatory/watermark_migration.go new file mode 100644 index 00000000..940db1d4 --- /dev/null +++ b/pkg/signatory/watermark_migration.go @@ -0,0 +1,170 @@ +package signatory + +import ( + "encoding/json" + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + + tz "github.com/ecadlabs/gotez" + "github.com/ecadlabs/gotez/b58" + "github.com/ecadlabs/signatory/pkg/hashmap" + "github.com/ecadlabs/signatory/pkg/signatory/request" +) + +type v0KindMap map[string]v0WatermarkMap +type v0WatermarkMap = hashmap.PublicKeyHashMap[*request.Watermark] + +const v0WatermarkDir = "watermark" + +func checkV0exist(baseDir string) (bool, error) { + filename := filepath.Join(baseDir, v0WatermarkDir) + _, err := os.Stat(filename) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return false, err + } + return false, nil + } + return true, nil +} + +func tryV0(baseDir string) (map[tz.ChainID]delegateMap, error) { + dir := filepath.Join(baseDir, v0WatermarkDir) + entries, err := os.ReadDir(dir) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + return nil, nil + } + + out := make(map[tz.ChainID]delegateMap) + for _, ent := range entries { + if !ent.Type().IsRegular() || !strings.HasSuffix(ent.Name(), ".json") { + continue + } + name := ent.Name() + chainID, err := b58.ParseChainID([]byte(name[:len(name)-5])) + if err != nil { + return nil, err + } + + filename := filepath.Join(dir, name) + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + var kinds v0KindMap + if err = json.NewDecoder(fd).Decode(&kinds); err != nil { + return nil, err + } + + outDelegates := make(delegateMap) + for kind, delegates := range kinds { + delegates.ForEach(func(key tz.PublicKeyHash, val *request.Watermark) bool { + kinds, ok := outDelegates.Get(key) + if !ok { + kinds = make(requestMap) + outDelegates.Insert(key, kinds) + } + kinds[kind] = val + return true + }) + } + out[*chainID] = outDelegates + } + + return out, nil +} + +type v1Watermark struct { + Level int32 `json:"level"` + Round tz.Option[int32] `json:"round"` + Order int `json:"order"` + Hash tz.Option[tz.BlockPayloadHash] `json:"hash"` +} + +type v1DelegateMap = hashmap.PublicKeyHashMap[*v1Watermark] + +const v1WatermarkDir = "watermark_v1" + +func checkV1exist(baseDir string) (bool, error) { + filename := filepath.Join(baseDir, v1WatermarkDir) + _, err := os.Stat(filename) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return false, err + } + return false, nil + } + return true, nil +} + +func tryV1(baseDir string) (map[tz.ChainID]delegateMap, error) { + dir := filepath.Join(baseDir, v1WatermarkDir) + entries, err := os.ReadDir(dir) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + return nil, nil + } + + out := make(map[tz.ChainID]delegateMap) + for _, ent := range entries { + if !ent.Type().IsRegular() || !strings.HasSuffix(ent.Name(), ".json") { + continue + } + name := ent.Name() + chainID, err := b58.ParseChainID([]byte(name[:len(name)-5])) + if err != nil { + return nil, err + } + + filename := filepath.Join(dir, name) + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + var delegates v1DelegateMap + if err = json.NewDecoder(fd).Decode(&delegates); err != nil { + return nil, err + } + + outDelegates := make(delegateMap) + delegates.ForEach(func(key tz.PublicKeyHash, val *v1Watermark) bool { + var req string + switch val.Order { + case 0: + req = "block" + case 1: + req = "preendorsement" + default: + req = "endorsement" + } + + wm := request.Watermark{ + Level: val.Level, + } + if val.Round.IsSome() { + wm.Round = val.Round.Unwrap() + } + if val.Hash.IsSome() { + hash := val.Hash.Unwrap() + wm.Hash = tz.Some(hash) + } + outDelegates.Insert(key, map[string]*request.Watermark{ + req: &wm, + }) + return true + }) + out[*chainID] = outDelegates + } + + return out, nil +} diff --git a/pkg/signatory/watermark_test.go b/pkg/signatory/watermark_test.go index 1c6df238..6e9a7697 100644 --- a/pkg/signatory/watermark_test.go +++ b/pkg/signatory/watermark_test.go @@ -3,6 +3,7 @@ package signatory import ( + "fmt" "os" "testing" @@ -13,13 +14,16 @@ import ( "github.com/stretchr/testify/require" ) -type msgMock request.Watermark - -func (m *msgMock) Watermark() *request.Watermark { - return (*request.Watermark)(m) +type dummyMsg struct { + kind string + level int32 + round int32 } -func (m *msgMock) RequestKind() string { return "dummy" } +func (r *dummyMsg) RequestKind() string { return r.kind } +func (r *dummyMsg) GetChainID() *tz.ChainID { return &tz.ChainID{} } +func (r *dummyMsg) GetLevel() int32 { return r.level } +func (r *dummyMsg) GetRound() int32 { return r.round } type testCase struct { pkh crypt.PublicKeyHash @@ -32,75 +36,97 @@ func TestWatermark(t *testing.T) { cases := []testCase{ { pkh: &tz.Ed25519PublicKeyHash{0}, - req: (*msgMock)(&request.Watermark{ - Chain: &tz.ChainID{}, - Level: request.Level{Level: 124}, - }), + req: &dummyMsg{ + kind: "kind0", + level: 124, + }, reqDigest: crypt.Digest{0}, expectErr: false, }, { pkh: &tz.Ed25519PublicKeyHash{0}, - req: (*msgMock)(&request.Watermark{ - Chain: &tz.ChainID{}, - Level: request.Level{Level: 123}, - }), + req: &dummyMsg{ + kind: "kind0", + level: 124, + }, reqDigest: crypt.Digest{1}, - expectErr: true, + expectErr: true, // same level + }, + { + pkh: &tz.Ed25519PublicKeyHash{0}, + req: &dummyMsg{ + kind: "kind0", + level: 123, + }, + reqDigest: crypt.Digest{2}, + expectErr: true, // level below }, { - // repeated request pkh: &tz.Ed25519PublicKeyHash{0}, - req: (*msgMock)(&request.Watermark{ - Chain: &tz.ChainID{}, - Level: request.Level{Level: 124}, - }), + req: &dummyMsg{ + kind: "kind0", + level: 124, + }, reqDigest: crypt.Digest{0}, - expectErr: false, + expectErr: false, // repeated request }, { pkh: &tz.Ed25519PublicKeyHash{1}, - req: (*msgMock)(&request.Watermark{ - Chain: &tz.ChainID{}, - Level: request.Level{Level: 124}, - }), + req: &dummyMsg{ + kind: "kind0", + level: 124, + }, reqDigest: crypt.Digest{3}, - expectErr: false, + expectErr: false, // different delegate }, { - pkh: &tz.Ed25519PublicKeyHash{0}, - req: (*msgMock)(&request.Watermark{ - Chain: &tz.ChainID{}, - Level: request.Level{Level: 125}, - }), + pkh: &tz.Ed25519PublicKeyHash{1}, + req: &dummyMsg{ + kind: "kind0", + level: 125, + }, reqDigest: crypt.Digest{4}, expectErr: false, }, + { + pkh: &tz.Ed25519PublicKeyHash{0}, + req: &dummyMsg{ + kind: "kind1", + level: 124, + }, + reqDigest: crypt.Digest{0}, + expectErr: false, // different kind + }, } t.Run("memory", func(t *testing.T) { var wm InMemoryWatermark - for _, c := range cases { - err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest) - if c.expectErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + for i, c := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest) + if c.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } }) t.Run("file", func(t *testing.T) { dir, err := os.MkdirTemp("", "watermark") require.NoError(t, err) - wm := FileWatermark{BaseDir: dir} - for _, c := range cases { - err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest) - if c.expectErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + wm, err := NewFileWatermark(dir) + require.NoError(t, err) + for i, c := range cases { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + err := wm.IsSafeToSign(c.pkh, c.req, &c.reqDigest) + if c.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) } }) }